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