displayablegraph.class.inc.php 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614
  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 = null, $y = null)
  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. $this->AddToStats($oNode, $aNodesPerClass);
  534. }
  535. else
  536. {
  537. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  538. }
  539. }
  540. foreach($aNodesPerClass as $sClass => $aDefs)
  541. {
  542. foreach($aDefs as $sStatus => $aGroupProps)
  543. {
  544. if (count($aGroupProps['nodes']) >= $iThresholdCount)
  545. {
  546. $oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus);
  547. $oNewNode->SetProperty('label', 'x'.count($aGroupProps['nodes']));
  548. $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
  549. $oNewNode->SetProperty('is_reached', ($sStatus == 'is_reached'));
  550. $oNewNode->SetProperty('class', $sClass);
  551. $oNewNode->SetProperty('count', count($aGroupProps['nodes']));
  552. $sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': '');
  553. $oNewNode = $oGraph->GetNode($sNewId);
  554. if ($oNewNode == null)
  555. {
  556. $oNewNode = new DisplayableGroupNode($oGraph, $sNewId);
  557. $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
  558. $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
  559. $oNewNode->SetProperty('class', $sClass);
  560. $oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
  561. $oNewNode->SetProperty('count', $aGroupProps['count']);
  562. }
  563. try
  564. {
  565. $oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
  566. }
  567. catch(Exception $e)
  568. {
  569. // Ignore this redundant egde
  570. }
  571. foreach($aGroupProps['nodes'] as $oNextNode)
  572. {
  573. $this->ReplaceNextNodeBy($oGraph, $oNextNode, $oNewNode, !$bDirectionUp);
  574. }
  575. //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  576. }
  577. else
  578. {
  579. foreach($aGroupProps['nodes'] as $oNode)
  580. {
  581. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  582. }
  583. }
  584. }
  585. }
  586. }
  587. }
  588. public function GetTooltip($aContextDefs)
  589. {
  590. $sHtml = '';
  591. $sHtml .= Dict::S('UI:RelationTooltip:Redundancy')."<hr>";
  592. $sHtml .= '<table><tbody>';
  593. $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>";
  594. $sHtml .= "<tr><td>".Dict::Format('UI:RelationTooltip:CriticalThreshold_N_of_M' , $this->GetProperty('threshold'), $this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>";
  595. $sHtml .= '</tbody></table>';
  596. return $sHtml;
  597. }
  598. public function GetObjectCount()
  599. {
  600. return 0;
  601. }
  602. public function GetObjectClass()
  603. {
  604. return null;
  605. }
  606. }
  607. class DisplayableEdge extends GraphEdge
  608. {
  609. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
  610. {
  611. $oSourceNode = $this->GetSourceNode();
  612. if (($oSourceNode->x == null) || ($oSourceNode->y == null))
  613. {
  614. return;
  615. }
  616. $xStart = $oSourceNode->x * $fScale;
  617. $yStart = $oSourceNode->y * $fScale;
  618. $oSinkNode = $this->GetSinkNode();
  619. if (($oSinkNode->x == null) || ($oSinkNode->y == null))
  620. {
  621. return;
  622. }
  623. $xEnd = $oSinkNode->x * $fScale;
  624. $yEnd = $oSinkNode->y * $fScale;
  625. $bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached'));
  626. $oPdf->setAlpha(1);
  627. if ($bReached)
  628. {
  629. $aColor = array(100, 100, 100);
  630. }
  631. else
  632. {
  633. $aColor = array(200, 200, 200);
  634. }
  635. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aColor));
  636. $oPdf->Line($xStart, $yStart, $xEnd, $yEnd);
  637. $vx = $xEnd - $xStart;
  638. $vy = $yEnd - $yStart;
  639. $l = sqrt($vx*$vx + $vy*$vy);
  640. $vx = $vx / $l;
  641. $vy = $vy / $l;
  642. $ux = -$vy;
  643. $uy = $vx;
  644. $lPos = max($l/2, $l - 40*$fScale);
  645. $iArrowSize = 5*$fScale;
  646. $x = $xStart + $lPos * $vx;
  647. $y = $yStart + $lPos * $vy;
  648. $oPdf->Line($x, $y, $x + $iArrowSize * ($ux-$vx), $y + $iArrowSize * ($uy-$vy));
  649. $oPdf->Line($x, $y, $x - $iArrowSize * ($ux+$vx), $y - $iArrowSize * ($uy+$vy));
  650. }
  651. }
  652. class DisplayableGroupNode extends DisplayableNode
  653. {
  654. protected $aObjects;
  655. public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0)
  656. {
  657. parent::__construct($oGraph, $sId, $x, $y);
  658. $this->aObjects = array();
  659. }
  660. public function AddObject(DBObject $oObj = null)
  661. {
  662. if (is_object($oObj))
  663. {
  664. $sPrevClass = $this->GetObjectClass();
  665. if (($sPrevClass !== null) && (get_class($oObj) !== $sPrevClass))
  666. {
  667. throw new Exception("Error: adding an object of class '".get_class($oObj)."' to a group of '$sPrevClass' objects.");
  668. }
  669. $this->aObjects[$oObj->GetKey()] = $oObj;
  670. }
  671. }
  672. public function GetObjects()
  673. {
  674. return $this->aObjects;
  675. }
  676. public function GetWidth()
  677. {
  678. return 50;
  679. }
  680. public function GetForRaphael($aContextDefs)
  681. {
  682. $aNode = array();
  683. $aNode['shape'] = 'group';
  684. $aNode['icon_url'] = $this->GetIconURL();
  685. $aNode['source'] = ($this->GetProperty('source') == true);
  686. $aNode['width'] = $this->GetWidth();
  687. $aNode['x'] = $this->x;
  688. $aNode['y']= $this->y;
  689. $aNode['label'] = $this->GetLabel();
  690. $aNode['id'] = $this->GetId();
  691. $aNode['group_index'] = $this->GetProperty('group_index'); // if supplied
  692. $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
  693. $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  694. $aNode['icon_attr'] = array('opacity' => $fTextOpacity);
  695. $aNode['disc_attr'] = array('stroke-width' => 2, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity);
  696. $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity);
  697. $aNode['tooltip'] = $this->GetTooltip($aContextDefs);
  698. return $aNode;
  699. }
  700. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
  701. {
  702. $bReached = $this->GetProperty('is_reached');
  703. $oPdf->SetFillColor(255, 255, 255);
  704. if ($bReached)
  705. {
  706. $aBorderColor = array(100, 100, 100);
  707. }
  708. else
  709. {
  710. $aBorderColor = array(200, 200, 200);
  711. }
  712. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor));
  713. $sIconUrl = $this->GetProperty('icon_url');
  714. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
  715. $oPdf->SetAlpha(1);
  716. $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF');
  717. if ($bReached)
  718. {
  719. $oPdf->SetAlpha(1);
  720. }
  721. else
  722. {
  723. $oPdf->SetAlpha(0.4);
  724. }
  725. $oPdf->Image($sIconPath, ($this->x - 17)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  726. $oPdf->Image($sIconPath, ($this->x + 1)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  727. $oPdf->Image($sIconPath, ($this->x -8)*$fScale, ($this->y +1)*$fScale, 16*$fScale, 16*$fScale);
  728. $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true);
  729. $width = $oPdf->GetStringWidth($this->GetProperty('label'));
  730. $oPdf->SetTextColor(0, 0, 0);
  731. $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label'));
  732. }
  733. public function GetTooltip($aContextDefs)
  734. {
  735. $sHtml = '';
  736. $iGroupIdx = $this->GetProperty('group_index');
  737. $sHtml .= '<a href="#" onclick="$(\'.itop-simple-graph\').simple_graph(\'show_group\', \'relation_group_'.$iGroupIdx.'\');">'.Dict::Format('UI:RelationGroupNumber_N', (1+$iGroupIdx))."</a>";
  738. $sHtml .= '<hr/>';
  739. $sHtml .= '<table><tbody><tr>';
  740. $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/>';
  741. $sHtml .= Dict::Format('UI_CountOfObjectsShort', $this->GetObjectCount()).'</td>';
  742. $sHtml .= '</tr></tbody></table>';
  743. return $sHtml;
  744. }
  745. public function GetObjectCount()
  746. {
  747. return count($this->aObjects);
  748. }
  749. public function GetObjectClass()
  750. {
  751. return ($this->GetObjectCount() > 0) ? get_class(reset($this->aObjects)) : null;
  752. }
  753. }
  754. /**
  755. * A Graph that can be displayed interactively using Raphael JS or saved as a PDF document
  756. */
  757. class DisplayableGraph extends SimpleGraph
  758. {
  759. protected $bDirectionDown;
  760. protected $aTempImages;
  761. protected $aSourceObjects;
  762. protected $aSinkObjects;
  763. public function __construct()
  764. {
  765. parent::__construct();
  766. $this->aTempImages = array();
  767. $this->aSourceObjects = array();
  768. $this->aSinkObjects = array();
  769. }
  770. public function GetTempImageName()
  771. {
  772. $sNewTempName = tempnam(APPROOT.'data', 'img-');
  773. $this->aTempImages[] = $sNewTempName;
  774. return $sNewTempName;
  775. }
  776. public function __destruct()
  777. {
  778. foreach($this->aTempImages as $sTempFile)
  779. {
  780. @unlink($sTempFile);
  781. }
  782. }
  783. /**
  784. * Build a DisplayableGraph from a RelationGraph
  785. * @param RelationGraph $oGraph
  786. * @param number $iGroupingThreshold
  787. * @param string $bDirectionDown
  788. * @return DisplayableGraph
  789. */
  790. public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
  791. {
  792. $oNewGraph = new DisplayableGraph();
  793. $oNewGraph->bDirectionDown = $bDirectionDown;
  794. $iPreviousTimeLimit = ini_get('max_execution_time');
  795. $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
  796. $oNodesIter = new RelationTypeIterator($oGraph, 'Node');
  797. foreach($oNodesIter as $oNode)
  798. {
  799. set_time_limit($iLoopTimeLimit);
  800. switch(get_class($oNode))
  801. {
  802. case 'RelationObjectNode':
  803. $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0);
  804. $oObj = $oNode->GetProperty('object');
  805. $sClass = get_class($oObj);
  806. if ($oNode->GetProperty('source'))
  807. {
  808. if (!array_key_exists($sClass, $oNewGraph->aSourceObjects))
  809. {
  810. $oNewGraph->aSourceObjects[$sClass] = array();
  811. }
  812. $oNewGraph->aSourceObjects[$sClass][] = $oObj->GetKey();
  813. $oNewNode->SetProperty('source', true);
  814. }
  815. if ($oNode->GetProperty('sink'))
  816. {
  817. if (!array_key_exists($sClass, $oNewGraph->aSinkObjects))
  818. {
  819. $oNewGraph->aSinkObjects[$sClass] = array();
  820. }
  821. $oNewGraph->aSinkObjects[$sClass][] = $oObj->GetKey();
  822. $oNewNode->SetProperty('sink', true);
  823. }
  824. $oNewNode->SetProperty('object', $oObj);
  825. $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
  826. $oNewNode->SetProperty('label', $oObj->GetRawName());
  827. $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter
  828. $oNewNode->SetProperty('is_reached_allowed', $oNode->GetProperty('is_reached_allowed'));
  829. $oNewNode->SetProperty('context_root_causes', $oNode->GetProperty('context_root_causes'));
  830. break;
  831. default:
  832. $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0);
  833. $iNbReached = (is_null($oNode->GetProperty('is_reached_count'))) ? 0 : $oNode->GetProperty('is_reached_count');
  834. $oNewNode->SetProperty('label', $iNbReached."/".($oNode->GetProperty('min_up') + $oNode->GetProperty('threshold')));
  835. $oNewNode->SetProperty('min_up', $oNode->GetProperty('min_up'));
  836. $oNewNode->SetProperty('threshold', $oNode->GetProperty('threshold'));
  837. $oNewNode->SetProperty('is_reached_count', $iNbReached);
  838. $oNewNode->SetProperty('is_reached', true);
  839. }
  840. }
  841. $oEdgesIter = new RelationTypeIterator($oGraph, 'Edge');
  842. foreach($oEdgesIter as $oEdge)
  843. {
  844. set_time_limit($iLoopTimeLimit);
  845. $oSourceNode = $oNewGraph->GetNode($oEdge->GetSourceNode()->GetId());
  846. $oSinkNode = $oNewGraph->GetNode($oEdge->GetSinkNode()->GetId());
  847. $oNewEdge = new DisplayableEdge($oNewGraph, $oEdge->GetId(), $oSourceNode, $oSinkNode);
  848. }
  849. // Remove duplicate edges between two nodes
  850. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  851. $aEdgeKeys = array();
  852. foreach($oEdgesIter as $oEdge)
  853. {
  854. set_time_limit($iLoopTimeLimit);
  855. $sSourceId = $oEdge->GetSourceNode()->GetId();
  856. $sSinkId = $oEdge->GetSinkNode()->GetId();
  857. if ($sSourceId == $sSinkId)
  858. {
  859. // Remove self referring edges
  860. $oNewGraph->_RemoveEdge($oEdge);
  861. }
  862. else
  863. {
  864. $sKey = $sSourceId.'//'.$sSinkId;
  865. if (array_key_exists($sKey, $aEdgeKeys))
  866. {
  867. // Remove duplicate edges
  868. $oNewGraph->_RemoveEdge($oEdge);
  869. }
  870. else
  871. {
  872. $aEdgeKeys[$sKey] = true;
  873. }
  874. }
  875. }
  876. $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node');
  877. foreach($oNodesIter as $oNode)
  878. {
  879. set_time_limit($iLoopTimeLimit);
  880. if ($bDirectionDown && $oNode->GetProperty('source'))
  881. {
  882. $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown);
  883. }
  884. else if (!$bDirectionDown && $oNode->GetProperty('sink'))
  885. {
  886. $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown);
  887. }
  888. }
  889. // Groups numbering
  890. $oIterator = new RelationTypeIterator($oNewGraph, 'Node');
  891. $iGroupIdx = 0;
  892. foreach($oIterator as $oNode)
  893. {
  894. set_time_limit($iLoopTimeLimit);
  895. if ($oNode instanceof DisplayableGroupNode)
  896. {
  897. if ($oNode->GetObjectCount() == 0)
  898. {
  899. // Remove empty groups
  900. $oNewGraph->_RemoveNode($oNode);
  901. }
  902. else
  903. {
  904. $aGroups[] = $oNode->GetObjects();
  905. $oNode->SetProperty('group_index', $iGroupIdx);
  906. $iGroupIdx++;
  907. }
  908. }
  909. }
  910. // Remove duplicate edges between two nodes
  911. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  912. $aEdgeKeys = array();
  913. foreach($oEdgesIter as $oEdge)
  914. {
  915. set_time_limit($iLoopTimeLimit);
  916. $sSourceId = $oEdge->GetSourceNode()->GetId();
  917. $sSinkId = $oEdge->GetSinkNode()->GetId();
  918. if ($sSourceId == $sSinkId)
  919. {
  920. // Remove self referring edges
  921. $oNewGraph->_RemoveEdge($oEdge);
  922. }
  923. else
  924. {
  925. $sKey = $sSourceId.'//'.$sSinkId;
  926. if (array_key_exists($sKey, $aEdgeKeys))
  927. {
  928. // Remove duplicate edges
  929. $oNewGraph->_RemoveEdge($oEdge);
  930. }
  931. else
  932. {
  933. $aEdgeKeys[$sKey] = true;
  934. }
  935. }
  936. }
  937. set_time_limit($iPreviousTimeLimit);
  938. return $oNewGraph;
  939. }
  940. /**
  941. * Initializes the positions by rendering using Graphviz in xdot format
  942. * and parsing the output.
  943. * @throws Exception
  944. */
  945. public function InitFromGraphviz()
  946. {
  947. $sDot = $this->DumpAsXDot();
  948. if (strpos($sDot, 'digraph') === false)
  949. {
  950. throw new Exception($sDot);
  951. }
  952. $aChunks = explode(";", $sDot);
  953. foreach($aChunks as $sChunk)
  954. {
  955. if(preg_match('/"([^"]+)".+pos="([0-9\\.]+),([0-9\\.]+)"/ms', $sChunk, $aMatches))
  956. {
  957. $sId = $aMatches[1];
  958. $xPos = $aMatches[2];
  959. $yPos = $aMatches[3];
  960. $oNode = $this->GetNode($sId);
  961. if ($oNode !== null)
  962. {
  963. $oNode->x = (float)$xPos;
  964. $oNode->y = (float)$yPos;
  965. }
  966. else
  967. {
  968. IssueLog::Warning("??? Position of the non-existing node '$sId', x=$xPos, y=$yPos");
  969. }
  970. }
  971. }
  972. }
  973. public function GetBoundingBox()
  974. {
  975. $xMin = null;
  976. $xMax = null;
  977. $yMin = null;
  978. $yMax = null;
  979. $oIterator = new RelationTypeIterator($this, 'Node');
  980. foreach($oIterator as $sId => $oNode)
  981. {
  982. if ($xMin === null) // First element in the loop
  983. {
  984. $xMin = $oNode->x - $oNode->GetWidth();
  985. $xMax = $oNode->x + $oNode->GetWidth();
  986. $yMin = $oNode->y - $oNode->GetHeight();
  987. $yMax = $oNode->y + $oNode->GetHeight();
  988. }
  989. else
  990. {
  991. $xMin = min($xMin, $oNode->x - $oNode->GetWidth() / 2);
  992. $xMax = max($xMax, $oNode->x + $oNode->GetWidth() / 2);
  993. $yMin = min($yMin, $oNode->y - $oNode->GetHeight() / 2);
  994. $yMax = max($yMax, $oNode->y + $oNode->GetHeight() / 2);
  995. }
  996. }
  997. return array('xmin' => $xMin, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
  998. }
  999. function Translate($dx, $dy)
  1000. {
  1001. $oIterator = new RelationTypeIterator($this, 'Node');
  1002. foreach($oIterator as $sId => $oNode)
  1003. {
  1004. $oNode->x += $dx;
  1005. $oNode->y += $dy;
  1006. }
  1007. }
  1008. public function UpdatePositions($aPositions)
  1009. {
  1010. foreach($aPositions as $sNodeId => $aPos)
  1011. {
  1012. $oNode = $this->GetNode($sNodeId);
  1013. if ($oNode != null)
  1014. {
  1015. $oNode->x = $aPos['x'];
  1016. $oNode->y = $aPos['y'];
  1017. }
  1018. }
  1019. }
  1020. /**
  1021. * Renders as JSON string suitable for loading into the simple_graph widget
  1022. */
  1023. function GetAsJSON($sContextKey)
  1024. {
  1025. $aContextDefs = static::GetContextDefinitions($sContextKey, false);
  1026. $aData = array('nodes' => array(), 'edges' => array(), 'groups' => array(), 'lists' => array());
  1027. $iGroupIdx = 0;
  1028. $oIterator = new RelationTypeIterator($this, 'Node');
  1029. foreach($oIterator as $sId => $oNode)
  1030. {
  1031. if ($oNode instanceof DisplayableGroupNode)
  1032. {
  1033. // The contents of the "Groups" tab will be rendered
  1034. // using a separate ajax call, since the content of
  1035. // the page is made of a mix of HTML / CSS / JS which
  1036. // cannot be conveyed easily in the JSON structure
  1037. // So we just pass a list of groups, each being defined by a class and a list of keys
  1038. // in order to avoid redoing the impact computation which is expensive
  1039. $aObjects = $oNode->GetObjects();
  1040. $aKeys = array();
  1041. foreach($aObjects as $oObj)
  1042. {
  1043. $sClass = get_class($oObj);
  1044. $aKeys[] = $oObj->GetKey();
  1045. }
  1046. $aData['groups'][$iGroupIdx] = array('class' => $sClass, 'keys' => $aKeys);
  1047. $oNode->SetProperty('group_index', $iGroupIdx);
  1048. $iGroupIdx++;
  1049. if ($oNode->GetProperty('is_reached'))
  1050. {
  1051. // Also add the objects from this group into the 'list' tab
  1052. if (!array_key_exists($sClass, $aData['lists']))
  1053. {
  1054. $aData['lists'][$sClass] = $aKeys;
  1055. }
  1056. else
  1057. {
  1058. $aData['lists'][$sClass] = array_merge($aData['lists'][$sClass], $aKeys);
  1059. }
  1060. }
  1061. }
  1062. if (($oNode instanceof DisplayableNode) && $oNode->GetProperty('is_reached') && is_object($oNode->GetProperty('object')))
  1063. {
  1064. $sObjClass = get_class($oNode->GetProperty('object'));
  1065. if (!array_key_exists($sObjClass, $aData['lists']))
  1066. {
  1067. $aData['lists'][$sObjClass] = array();
  1068. }
  1069. $aData['lists'][$sObjClass][] = $oNode->GetProperty('object')->GetKey();
  1070. }
  1071. $aData['nodes'][] = $oNode->GetForRaphael($aContextDefs);
  1072. }
  1073. uksort($aData['lists'], array(get_class($this), 'SortOnClassLabel')); // sort on the localized names of the classes to provide a consistent and stable order
  1074. $oIterator = new RelationTypeIterator($this, 'Edge');
  1075. foreach($oIterator as $sId => $oEdge)
  1076. {
  1077. $aEdge = array();
  1078. $aEdge['id'] = $oEdge->GetId();
  1079. $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
  1080. $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
  1081. $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
  1082. $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
  1083. $aData['edges'][] = $aEdge;
  1084. }
  1085. return json_encode($aData);
  1086. }
  1087. /**
  1088. * Sort class "codes" based on their localized name
  1089. * @param string $sClass1
  1090. * @param string $sClass2
  1091. * @return number -1, 0 or 1
  1092. */
  1093. public static function SortOnClassLabel($sClass1, $sClass2)
  1094. {
  1095. return strcasecmp(MetaModel::GetName($sClass1), MetaModel::GetName($sClass2));
  1096. }
  1097. /**
  1098. * Renders the graph in a PDF document: centered in the current page
  1099. * @param PDFPage $oPage The PDFPage representing the PDF document to draw into
  1100. * @param string $sComments An optional comment to display next to the graph (HTML entities will be escaped, \n replaced by <br/>)
  1101. * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down
  1102. * @param float $xMin Left coordinate of the bounding box to display the graph
  1103. * @param float $xMax Right coordinate of the bounding box to display the graph
  1104. * @param float $yMin Top coordinate of the bounding box to display the graph
  1105. * @param float $yMax Bottom coordinate of the bounding box to display the graph
  1106. */
  1107. function RenderAsPDF(PDFPage $oPage, $sComments = '', $sContextKey, $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1)
  1108. {
  1109. $aContextDefs = static::GetContextDefinitions($sContextKey, false); // No need to develop the parameters
  1110. $oPdf = $oPage->get_tcpdf();
  1111. $aBB = $this->GetBoundingBox();
  1112. $this->Translate(-$aBB['xmin'], -$aBB['ymin']);
  1113. $aMargins = $oPdf->getMargins();
  1114. if ($xMin == -1)
  1115. {
  1116. $xMin = $aMargins['left'];
  1117. }
  1118. if ($xMax == -1)
  1119. {
  1120. $xMax = $oPdf->getPageWidth() - $aMargins['right'];
  1121. }
  1122. if ($yMin == -1)
  1123. {
  1124. $yMin = $aMargins['top'];
  1125. }
  1126. if ($yMax == -1)
  1127. {
  1128. $yMax = $oPdf->getPageHeight() - $aMargins['bottom'];
  1129. }
  1130. $fBreakMargin = $oPdf->getBreakMargin();
  1131. $oPdf->SetAutoPageBreak(false);
  1132. $aRemainingArea = $this->RenderKey($oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs);
  1133. $xMin = $aRemainingArea['xmin'];
  1134. $xMax = $aRemainingArea['xmax'];
  1135. $yMin = $aRemainingArea['ymin'];
  1136. $yMax = $aRemainingArea['ymax'];
  1137. //$oPdf->Rect($xMin, $yMin, $xMax - $xMin, $yMax - $yMin, 'D', array(), array(225, 50, 50));
  1138. $fPageW = $xMax - $xMin;
  1139. $fPageH = $yMax - $yMin;
  1140. $w = $aBB['xmax'] - $aBB['xmin'];
  1141. $h = $aBB['ymax'] - $aBB['ymin'] + 10; // Extra space for the labels which may appear "below" the icons
  1142. $fScale = min($fPageW / $w, $fPageH / $h);
  1143. $dx = ($fPageW - $fScale * $w) / 2;
  1144. $dy = ($fPageH - $fScale * $h) / 2;
  1145. $this->Translate(($xMin + $dx)/$fScale, ($yMin + $dy)/$fScale);
  1146. $oIterator = new RelationTypeIterator($this, 'Edge');
  1147. $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
  1148. foreach($oIterator as $sId => $oEdge)
  1149. {
  1150. set_time_limit($iLoopTimeLimit);
  1151. $oEdge->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs);
  1152. }
  1153. $oIterator = new RelationTypeIterator($this, 'Node');
  1154. foreach($oIterator as $sId => $oNode)
  1155. {
  1156. set_time_limit($iLoopTimeLimit);
  1157. $oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs);
  1158. }
  1159. $oPdf->SetAutoPageBreak(true, $fBreakMargin);
  1160. $oPdf->SetAlpha(1);
  1161. }
  1162. /**
  1163. * Renders (in PDF) the key (legend) of the graphics vertically to the left of the specified zone (xmin,ymin, xmax,ymax),
  1164. * and the comment (if any) at the bottom of the page. Returns the position of remaining area.
  1165. * @param TCPDF $oPdf
  1166. * @param string $sComments
  1167. * @param float $xMin
  1168. * @param float $yMin
  1169. * @param float $xMax
  1170. * @param float $yMax
  1171. * @param hash $aContextDefs
  1172. * @return hash An array ('xmin' => , 'xmax' => ,'ymin' => , 'ymax' => ) of the remaining available area to paint the graph
  1173. */
  1174. protected function RenderKey(TCPDF $oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs)
  1175. {
  1176. $fFontSize = 7; // in mm
  1177. $fIconSize = 6; // in mm
  1178. $fPadding = 1; // in mm
  1179. $oIterator = new RelationTypeIterator($this, 'Node');
  1180. $fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - $fIconSize, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - $fIconSize);
  1181. $aClasses = array();
  1182. $aIcons = array();
  1183. $aContexts = array();
  1184. $aContextIcons = array();
  1185. $oPdf->SetFont('dejavusans', '', $fFontSize, '', true);
  1186. foreach($oIterator as $sId => $oNode)
  1187. {
  1188. if ($sClass = $oNode->GetObjectClass())
  1189. {
  1190. if (!array_key_exists($sClass, $aClasses))
  1191. {
  1192. $sClassLabel = MetaModel::GetName($sClass);
  1193. $width = $oPdf->GetStringWidth($sClassLabel);
  1194. $fMaxWidth = max($width, $fMaxWidth);
  1195. $aClasses[$sClass] = $sClassLabel;
  1196. $sIconUrl = $oNode->GetProperty('icon_url');
  1197. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
  1198. $aIcons[$sClass] = $sIconPath;
  1199. }
  1200. }
  1201. $aContextRootCauses = $oNode->GetProperty('context_root_causes');
  1202. if (!is_null($aContextRootCauses))
  1203. {
  1204. foreach($aContextRootCauses as $key => $aObjects)
  1205. {
  1206. $aContexts[$key] = Dict::S($aContextDefs[$key]['dict']);
  1207. $aContextIcons[$key] = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon'];
  1208. }
  1209. }
  1210. }
  1211. $oPdf->SetXY($xMin + $fPadding, $yMin + $fPadding);
  1212. $yPos = $yMin + $fPadding;
  1213. $oPdf->SetFillColor(225, 225, 225);
  1214. $oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
  1215. $yPos += $fIconSize + 2*$fPadding;
  1216. foreach($aClasses as $sClass => $sLabel)
  1217. {
  1218. $oPdf->SetX($xMin + $fIconSize + $fPadding);
  1219. $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */);
  1220. $oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, $fIconSize, $fIconSize);
  1221. $yPos += $fIconSize + 2*$fPadding;
  1222. }
  1223. foreach($aContexts as $key => $sLabel)
  1224. {
  1225. $oPdf->SetX($xMin + $fIconSize + $fPadding);
  1226. $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */);
  1227. $oPdf->Image($aContextIcons[$key], $xMin+1+$fIconSize*0.125, $yPos+$fIconSize*0.125, $fIconSize*0.75, $fIconSize*0.75);
  1228. $yPos += $fIconSize + 2*$fPadding;
  1229. }
  1230. $oPdf->Rect($xMin, $yMin, $fMaxWidth + $fIconSize + 3*$fPadding, $yMax - $yMin, 'D');
  1231. if ($sComments != '')
  1232. {
  1233. // Draw the comment text (surrounded by a rectangle)
  1234. $xPos = $xMin + $fMaxWidth + $fIconSize + 4*$fPadding;
  1235. $w = $xMax - $xPos - 2*$fPadding;
  1236. $iNbLines = 1;
  1237. $sText = '<p>'.str_replace("\n", '<br/>', htmlentities($sComments, ENT_QUOTES, 'UTF-8'), $iNbLines).'</p>';
  1238. $fLineHeight = $oPdf->getStringHeight($w, $sText);
  1239. $h = (1+$iNbLines) * $fLineHeight;
  1240. $yPos = $yMax - 2*$fPadding - $h;
  1241. $oPdf->writeHTMLCell($w, $h, $xPos + $fPadding, $yPos + $fPadding, $sText, 0 /* border */, 1 /* ln */);
  1242. $oPdf->Rect($xPos, $yPos, $w + 2*$fPadding, $h + 2*$fPadding, 'D');
  1243. $yMax = $yPos - $fPadding;
  1244. }
  1245. return array('xmin' => $xMin + $fMaxWidth + $fIconSize + 4*$fPadding, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
  1246. }
  1247. /**
  1248. * Get the context definitions from the parameters / configuration. The format of the "key" string is:
  1249. * <module>/relation_context/<class>/<relation>/<direction>
  1250. * The values will be retrieved for the given class and all its parents and merged together as a single array.
  1251. * Entries with an invalid query are removed from the list.
  1252. * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down
  1253. * @param bool $bDevelopParams Whether or not to substitute the parameters inside the queries with the supplied "context params"
  1254. * @param array $aContextParams Arguments for the queries (via ToArgs()) if $bDevelopParams == true
  1255. * @return multitype:multitype:string
  1256. */
  1257. public static function GetContextDefinitions($sContextKey, $bDevelopParams = true, $aContextParams = array())
  1258. {
  1259. $aContextDefs = array();
  1260. $aLevels = explode('/', $sContextKey);
  1261. if (count($aLevels) < 5)
  1262. {
  1263. IssueLog::Warning("GetContextDefinitions: invalid 'sContextKey' = '$sContextKey'. 5 levels of / are expected !");
  1264. }
  1265. else
  1266. {
  1267. $sLeafClass = $aLevels[2];
  1268. if (!MetaModel::IsValidClass($sLeafClass))
  1269. {
  1270. IssueLog::Warning("GetContextDefinitions: invalid 'sLeafClass' = '$sLeafClass'. A valid class name is expected in 3rd position inside '$sContextKey' !");
  1271. }
  1272. else
  1273. {
  1274. $aRelationContext = MetaModel::GetConfig()->GetModuleSetting($aLevels[0], $aLevels[1], array());
  1275. foreach(MetaModel::EnumParentClasses($sLeafClass, ENUM_PARENT_CLASSES_ALL) as $sClass)
  1276. {
  1277. if (isset($aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items']))
  1278. {
  1279. $aContextDefs = array_merge($aContextDefs, $aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items']);
  1280. }
  1281. }
  1282. // Check if the queries are valid
  1283. foreach($aContextDefs as $sKey => $sDefs)
  1284. {
  1285. $sOQL = $aContextDefs[$sKey]['oql'];
  1286. try
  1287. {
  1288. // Expand the parameters. If anything goes wrong, then the query is considered as invalid and removed from the list
  1289. $oSearch = DBObjectSearch::FromOQL($sOQL);
  1290. $aContextDefs[$sKey]['oql'] = $oSearch->ToOQL($bDevelopParams, $aContextParams);
  1291. }
  1292. catch(Exception $e)
  1293. {
  1294. IssueLog::Warning('Invalid OQL query: '.$sOQL.' in the parameter '.$sContextKey);
  1295. unset($aContextDefs[$sKey]);
  1296. }
  1297. }
  1298. }
  1299. }
  1300. return $aContextDefs;
  1301. }
  1302. /**
  1303. * Display the graph inside the given page, with the "filter" drawer above it
  1304. * @param WebPage $oP
  1305. * @param hash $aResults
  1306. * @param string $sRelation
  1307. * @param ApplicationContext $oAppContext
  1308. * @param array $aExcludedObjects
  1309. */
  1310. function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null, $sContextKey, $aContextParams = array())
  1311. {
  1312. $aContextDefs = static::GetContextDefinitions($sContextKey, true, $aContextParams);
  1313. $aExcludedByClass = array();
  1314. foreach($aExcludedObjects as $oObj)
  1315. {
  1316. if (!array_key_exists(get_class($oObj), $aExcludedByClass))
  1317. {
  1318. $aExcludedByClass[get_class($oObj)] = array();
  1319. }
  1320. $aExcludedByClass[get_class($oObj)][] = $oObj->GetKey();
  1321. }
  1322. $oP->add("<div class=\"not-printable\">\n");
  1323. $oP->add("<div id=\"ds_flash\" class=\"SearchDrawer\" style=\"display:none;\">\n");
  1324. if (!$oP->IsPrintableVersion())
  1325. {
  1326. $oP->add_ready_script(
  1327. <<<EOF
  1328. $( "#tabbedContent_0" ).tabs({ heightStyle: "fill" });
  1329. EOF
  1330. );
  1331. }
  1332. $oP->add_ready_script(
  1333. <<<EOF
  1334. $("#dh_flash").click( function() {
  1335. $("#ds_flash").slideToggle('normal', function() { $("#ds_flash").parent().resize(); $("#dh_flash").trigger('toggle_complete'); } );
  1336. $("#dh_flash").toggleClass('open');
  1337. });
  1338. $('#ReloadMovieBtn').button().button('disable');
  1339. EOF
  1340. );
  1341. $aSortedElements = array();
  1342. foreach($aResults as $sClassIdx => $aObjects)
  1343. {
  1344. foreach($aObjects as $oCurrObj)
  1345. {
  1346. $sSubClass = get_class($oCurrObj);
  1347. $aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass);
  1348. }
  1349. }
  1350. asort($aSortedElements);
  1351. $idx = 0;
  1352. foreach($aSortedElements as $sSubClass => $sClassName)
  1353. {
  1354. $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> ");
  1355. $idx++;
  1356. }
  1357. $oP->add("<p style=\"text-align:right\"><button type=\"button\" id=\"ReloadMovieBtn\" onClick=\"DoReload()\">".Dict::S('UI:Button:Refresh')."</button></p>");
  1358. $oP->add("</div>\n");
  1359. $oP->add("<div class=\"HRDrawer\"></div>\n");
  1360. $oP->add("<div id=\"dh_flash\" class=\"DrawerHandle\">".Dict::S('UI:ElementsDisplayed')."</div>\n");
  1361. $oP->add("</div>\n"); // class="not-printable"
  1362. $aAdditionalContexts = array();
  1363. foreach($aContextDefs as $sKey => $aDefinition)
  1364. {
  1365. $aAdditionalContexts[] = array('key' => $sKey, 'label' => Dict::S($aDefinition['dict']), 'oql' => $aDefinition['oql'], 'default' => (array_key_exists('default', $aDefinition) && ($aDefinition['default'] == 'yes')));
  1366. }
  1367. $sDirection = utils::ReadParam('d', 'horizontal');
  1368. $iGroupingThreshold = utils::ReadParam('g', 5);
  1369. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
  1370. $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
  1371. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
  1372. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
  1373. try
  1374. {
  1375. $this->InitFromGraphviz();
  1376. $sExportAsPdfURL = '';
  1377. $sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1378. $oAppcontext = new ApplicationContext();
  1379. $sContext = $oAppContext->GetForLink();
  1380. $sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
  1381. $sExportAsDocumentURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_attachment&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1382. $sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1383. $sAttachmentExportTitle = '';
  1384. if (($sObjClass != null) && ($iObjKey != null))
  1385. {
  1386. $oTargetObj = MetaModel::GetObject($sObjClass, $iObjKey, false);
  1387. if ($oTargetObj)
  1388. {
  1389. $sAttachmentExportTitle = Dict::Format('UI:Relation:AttachmentExportOptions_Name', $oTargetObj->GetName());
  1390. }
  1391. }
  1392. $sId = 'graph';
  1393. $sStyle = '';
  1394. if ($oP->IsPrintableVersion())
  1395. {
  1396. // Optimize for printing on A4/Letter vertically
  1397. $sStyle = 'margin-left:auto; margin-right:auto;';
  1398. $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
  1399. }
  1400. $oP->add('<div id="'.$sId.'" class="simple-graph" style="'.$sStyle.'"></div>');
  1401. $aParams = array(
  1402. 'source_url' => $sLoadFromURL,
  1403. 'sources' => ($this->bDirectionDown ? $this->aSourceObjects : $this->aSinkObjects),
  1404. 'excluded' => $aExcludedByClass,
  1405. 'grouping_threshold' => $iGroupingThreshold,
  1406. 'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
  1407. 'export_as_attachment' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsAttachment'), 'obj_class' => $sObjClass, 'obj_key' => $iObjKey),
  1408. 'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
  1409. 'labels' => array(
  1410. 'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
  1411. 'export_as_attachment_title' => $sAttachmentExportTitle,
  1412. 'export' => Dict::S('UI:Button:Export'),
  1413. 'cancel' => Dict::S('UI:Button:Cancel'),
  1414. 'title' => Dict::S('UI:RelationOption:Title'),
  1415. 'untitled' => Dict::S('UI:RelationOption:Untitled'),
  1416. 'include_list' => Dict::S('UI:RelationOption:IncludeList'),
  1417. 'comments' => Dict::S('UI:RelationOption:Comments'),
  1418. 'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'),
  1419. 'refresh' => Dict::S('UI:Button:Refresh'),
  1420. 'check_all' => Dict::S('UI:SearchValue:CheckAll'),
  1421. 'uncheck_all' => Dict::S('UI:SearchValue:UncheckAll'),
  1422. 'none_selected' => Dict::S('UI:Relation:NoneSelected'),
  1423. 'nb_selected' => Dict::S('UI:SearchValue:NbSelected'),
  1424. 'additional_context_info' => Dict::S('UI:Relation:AdditionalContextInfo'),
  1425. 'zoom' => Dict::S('UI:Relation:Zoom'),
  1426. 'loading' => Dict::S('UI:Loading'),
  1427. ),
  1428. 'page_format' => array(
  1429. 'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
  1430. 'values' => array(
  1431. 'A3' => Dict::S('UI:PageFormat_A3'),
  1432. 'A4' => Dict::S('UI:PageFormat_A4'),
  1433. 'Letter' => Dict::S('UI:PageFormat_Letter'),
  1434. ),
  1435. ),
  1436. 'page_orientation' => array(
  1437. 'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
  1438. 'values' => array(
  1439. 'P' => Dict::S('UI:PageOrientation_Portrait'),
  1440. 'L' => Dict::S('UI:PageOrientation_Landscape'),
  1441. ),
  1442. ),
  1443. 'additional_contexts' => $aAdditionalContexts,
  1444. 'context_key' => $sContextKey,
  1445. );
  1446. if (!extension_loaded('gd'))
  1447. {
  1448. // PDF export requires GD
  1449. unset($aParams['export_as_pdf']);
  1450. }
  1451. if (!extension_loaded('gd') || is_null($sObjClass) || is_null($iObjKey))
  1452. {
  1453. // Export as Attachment requires GD (for building the PDF) AND a valid objclass/objkey couple
  1454. unset($aParams['export_as_attachment']);
  1455. }
  1456. $oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");");
  1457. }
  1458. catch(Exception $e)
  1459. {
  1460. $oP->add('<div>'.$e->getMessage().'</div>');
  1461. }
  1462. $oP->add_script(
  1463. <<<EOF
  1464. function DoReload()
  1465. {
  1466. $('#ReloadMovieBtn').button('disable');
  1467. try
  1468. {
  1469. var aExcluded = [];
  1470. $('input[name^=excluded]').each( function() {
  1471. if (!$(this).prop('checked'))
  1472. {
  1473. aExcluded.push($(this).val());
  1474. }
  1475. } );
  1476. $('#graph').simple_graph('option', {excluded_classes: aExcluded});
  1477. $('#graph').simple_graph('reload');
  1478. }
  1479. catch(err)
  1480. {
  1481. alert(err);
  1482. }
  1483. }
  1484. EOF
  1485. );
  1486. }
  1487. }