Ver código fonte

Relation diagrams:
- Localization
- Handle the resize of the window
- Aysnchronous load/reload
- Filtering of the result based on the class

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3574 a333f486-631f-4898-b8df-5754b55c2be0

dflaven 10 anos atrás
pai
commit
c75766edb2

+ 17 - 1
application/itopwebpage.class.inc.php

@@ -171,6 +171,7 @@ EOF;
 									var innerWidth = $(this).innerWidth() - 10;
 									$(this).find('.item').width(innerWidth);
 								});
+								$('.panel-resized').trigger('resized');
 						}
 				
 					}
@@ -217,7 +218,7 @@ EOF;
 			},
 			beforeLoad: function( event, ui ) {
 				if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) {
-					event.preventDefault();
+					event.defaultPrevented = true;
 					return;
 				}
 				ui.panel.html('<div><img src="../images/indicator.gif"></div>');
@@ -297,6 +298,21 @@ EOF
 		$.bbq.pushState( state );
 	});
 	
+	// refresh the hash when the tab is changed (from a JS script)
+	$('body').on( 'tabsactivate', '.ui-tabs', function(event, ui) {
+		var state = {};
+			
+		// Get the id of this tab widget.
+		var id = $(ui.newTab).closest( 'div[id^=tabbedContent]' ).attr( 'id' );
+		
+		// Get the index of this tab.
+		var idx = $(ui.newTab).prevAll().length;
+			
+		// Set the state!
+		state[ id ] = idx;
+		$.bbq.pushState( state );
+	});
+	
 	// Bind an event to window.onhashchange that, when the history state changes,
 	// iterates over all tab widgets, changing the current tab as necessary.
 	$(window).bind( 'hashchange', function(e)

+ 84 - 8
core/displayablegraph.class.inc.php

@@ -584,6 +584,13 @@ class DisplayableGraph extends SimpleGraph
 		}
 	}
 	
+	/**
+	 * Build a DisplayableGraph from a RelationGraph
+	 * @param RelationGraph $oGraph
+	 * @param number $iGroupingThreshold
+	 * @param string $bDirectionDown
+	 * @return DisplayableGraph
+	 */
 	public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
 	{
 		$oNewGraph = new DisplayableGraph();
@@ -697,6 +704,11 @@ class DisplayableGraph extends SimpleGraph
 		return $oNewGraph;
 	}
 	
+	/**
+	 * Initializes the positions by rendering using Graphviz in xdot format
+	 * and parsing the output.
+	 * @throws Exception
+	 */
 	public function InitFromGraphviz()
 	{
 		$sDot = $this->DumpAsXDot();
@@ -780,22 +792,45 @@ class DisplayableGraph extends SimpleGraph
 		}
 	}
 	
+	/**
+	 * Renders as a suite of Javascript instructions to display the graph using the simple_graph widget
+	 * @param WebPage $oP
+	 * @param string $sId
+	 * @param string $sExportAsPdfURL
+	 * @param string $sExportAsDocumentURL
+	 * @param string $sDrillDownURL
+	 */
 	function RenderAsRaphael(WebPage $oP, $sId = null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL)
 	{
 		if ($sId == null)
 		{
 			$sId = 'graph';
 		}
-		$aBB = $this->GetBoundingBox();
 		$oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
 		$aParams = array(
-			'xmin' => $aBB['xmin'],
-			'xmax' => $aBB['xmax'],
-			'ymin' => $aBB['ymin'],
-			'ymax' => $aBB['ymax'],
-			'export_as_pdf_url' => $sExportAsPdfURL,
-			'export_as_document_url' => $sExportAsDocumentURL,
-			'drill_down_url' => $sDrillDownURL,
+			'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
+			'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
+			'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
+			'labels' => array(
+				'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
+				'export' => Dict::S('UI:Relation:PDFExportOptions'),
+				'cancel' => Dict::S('UI:Button:Cancel'),
+			),
+			'page_format' => array(
+				'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
+				'values' => array(
+					'A3' => Dict::S('UI:PageFormat_A3'),
+					'A4' => Dict::S('UI:PageFormat_A4'),
+					'Letter' => Dict::S('UI:PageFormat_Letter'),
+				),
+			),
+			'page_orientation' => array(
+				'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
+				'values' => array(
+					'P' => Dict::S('UI:PageOrientation_Portrait'),
+					'L' => Dict::S('UI:PageOrientation_Landscape'),
+				),
+			),
 		);
 		$oP->add_ready_script("var oGraph = $('#$sId').simple_graph(".json_encode($aParams).");");
 		
@@ -822,6 +857,47 @@ class DisplayableGraph extends SimpleGraph
 		$oP->add_ready_script("oGraph.simple_graph('draw');");
 	}
 
+	/**
+	 * Renders as JSON string suitable for loading into the simple_graph widget
+	 */
+	function GetAsJSON()
+	{
+		$aData = array('nodes' => array(), 'edges' => array());
+		$iGroupIdx = 0;
+		$oIterator = new RelationTypeIterator($this, 'Node');
+		foreach($oIterator as $sId => $oNode)
+		{
+			if ($oNode instanceof DisplayableGroupNode)
+			{
+				$aGroups[] = $oNode->GetObjects();
+				$oNode->SetProperty('group_index', $iGroupIdx);
+				$iGroupIdx++;
+			}
+			$aData['nodes'][] = $oNode->GetForRaphael();
+		}
+		
+		$oIterator = new RelationTypeIterator($this, 'Edge');
+		foreach($oIterator as $sId => $oEdge)
+		{
+			$aEdge = array();
+			$aEdge['id'] = $oEdge->GetId();
+			$aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
+			$aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
+			$fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
+			$aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
+			$aData['edges'][] = $aEdge;
+		}
+	
+		return json_encode($aData);
+	}
+	
+	/**
+	 * Renders the graph as a PDF file
+	 * @param WebPage $oP The page for the ouput of the PDF
+	 * @param string $sTitle The title of the PDF
+	 * @param string $sPageFormat The page format: A4, A3, Letter...
+	 * @param string $sPageOrientation The orientation of the page (L = Landscape, P = Portrait)
+	 */
 	function RenderAsPDF(WebPage $oP, $sTitle = 'Untitled', $sPageFormat = 'A4', $sPageOrientation = 'P')
 	{
 		require_once(APPROOT.'lib/tcpdf/tcpdf.php');

+ 14 - 1
dictionaries/dictionary.itop.ui.php

@@ -965,9 +965,22 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:DisplayThisMessageAtStartup' => 'Display this message at startup',
 	'UI:RelationshipGraph' => 'Graphical view',
 	'UI:RelationshipList' => 'List',
+	'UI:RelationGroups' => 'Groups',
 	'UI:OperationCancelled' => 'Operation Cancelled',
 	'UI:ElementsDisplayed' => 'Filtering',
-
+	'UI:RelationGroupNumber_N' => 'Group #%1$d',
+	'UI:Relation:ExportAsPDF' => 'Export as PDF...',
+	'UI:Relation:ExportAsDocument' => 'Export as Document...',
+	'UI:Relation:DrillDown' => 'Details...',
+	'UI:Relation:PDFExportOptions' => 'PDF Export Options',
+	'UI:Button:Export' => 'Export',
+	'UI:Relation:PDFExportPageFormat' => 'Page format',
+	'UI:PageFormat_A3' => 'A3',
+	'UI:PageFormat_A4' => 'A4',
+	'UI:PageFormat_Letter' => 'Letter',
+	'UI:Relation:PDFExportPageOrientation' => 'Page orientation',
+	'UI:PageOrientation_Portrait' => 'Portrait',
+	'UI:PageOrientation_Landscape' => 'Landscape',	
 	'Portal:Title' => 'iTop user portal',
 	'Portal:NoRequestMgmt' => 'Dear %1$s, you have been redirected to this page because your account is configured with the profile \'Portal user\'. Unfortunately, iTop has not been installed with the feature \'Request Management\'. Please contact your administrator.',
 	'Portal:Refresh' => 'Refresh',

+ 14 - 0
dictionaries/fr.dictionary.itop.ui.php

@@ -809,7 +809,21 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:DisplayThisMessageAtStartup' => 'Afficher ce message au démarrage',
 	'UI:RelationshipGraph' => 'Vue graphique',
 	'UI:RelationshipList' => 'Liste',
+	'UI:RelationGroups' => 'Groupes',
 	'UI:ElementsDisplayed' => 'Filtrage',
+	'UI:RelationGroupNumber_N' => 'Groupe n°%1$d',
+	'UI:Relation:ExportAsPDF' => 'Exportation en PDF...',
+	'UI:Relation:ExportAsDocument' => 'Exportation comme Document...',
+	'UI:Relation:DrillDown' => 'Détails...',
+	'UI:Relation:PDFExportOptions' => 'Options de l\'Exportation PDF',
+	'UI:Button:Export' => 'Exporter',
+	'UI:Relation:PDFExportPageFormat' => 'Format de page',
+	'UI:PageFormat_A3' => 'A3',
+	'UI:PageFormat_A4' => 'A4',
+	'UI:PageFormat_Letter' => 'Letter',
+	'UI:Relation:PDFExportPageOrientation' => 'Orientation de la page',
+	'UI:PageOrientation_Portrait' => 'Portrait',
+	'UI:PageOrientation_Landscape' => 'Paysage',
 	'UI:OperationCancelled' => 'Opération Annulée',
 	'Portal:Title' => 'Portail utilisateur iTop',
 	'Portal:NoRequestMgmt' => 'Chèr(e) %1$s, vous avez été redirigé(e) vers cette page car votre compte utilisateur est configuré avec le profil \'Utilisateur du Portail\'. Malheureusement, iTop n\'a pas été installé avec le module de \'Gestion des Demandes\'. Merci de contacter votre administrateur iTop.',

+ 113 - 35
js/simple_graph.js

@@ -13,15 +13,16 @@ $(function()
 		// default options
 		options:
 		{
-			xmin: 0,
-			xmax: 0,
-			ymin: 0,
-			ymax: 0,
 			align: 'center',
 			'vertical-align': 'middle',
-			export_as_pdf_url: '',
-			export_as_document_url: '',
-			drill_down_url: '',
+			source_url: null,
+			export_as_pdf: null,
+			page_format: { label: 'Page Format:', values: { A3: 'A3', A4: 'A4', Letter: 'Letter' }, 'default': 'A4'},
+			page_orientation: { label: 'Page Orientation:', values: { P: 'Portait', L: 'Landscape' }, 'default': 'L' },
+			labels: { export_pdf_title: 'PDF Export Options', cancel: 'Cancel', 'export': 'Export' },
+			export_as_document: null,
+			drill_down: null,
+			excluded: []
 		},
 	
 		// the constructor
@@ -35,17 +36,21 @@ $(function()
 			this.yOffset = 0;
 			this.iTextHeight = 12;
 			
-			this.auto_scale();
 			this.oPaper = Raphael(this.element.get(0), this.element.width(), this.element.height());
 
 			this.element
+			.addClass('panel-resized')
 			.addClass('itop-simple-graph')
 			.addClass('graph');
 			
 			this._create_toolkit_menu();
 			this._build_context_menus();
+			$(window).bind('resized', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } );
+			if (this.options.source_url != null)
+			{
+				this.load_from_url(this.options.source_url);
+			}
 		},
-	
 		// called when created, and later when changing options
 		_refresh: function()
 		{
@@ -76,13 +81,17 @@ $(function()
 		},
 		draw: function()
 		{
+			this._updateBBox();
+			this.auto_scale();
 			this.oPaper.clear();
 			for(var k in this.aNodes)
 			{
+				this.aNodes[k].aElements = [];
 				this._draw_node(this.aNodes[k]);
 			}
 			for(var k in this.aEdges)
 			{
+				this.aEdges[k].aElements = [];
 				this._draw_edge(this.aEdges[k]);
 			}
 		},
@@ -90,6 +99,7 @@ $(function()
 		{
 			var iWidth = oNode.width;
 			var iHeight = 32;
+			var iFontSize = 10;
 			var xPos = Math.round(oNode.x * this.fZoom + this.xOffset);
 			var yPos = Math.round(oNode.y * this.fZoom + this.yOffset);
 			oNode.tx = 0;
@@ -99,8 +109,9 @@ $(function()
 				case 'disc':
 				oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
 				var oText = this.oPaper.text(xPos, yPos, oNode.label);
+				oNode.text_attr['font-size'] = iFontSize * this.fZoom;
 				oText.attr(oNode.text_attr);
-				oText.transform('s'+this.fZoom);
+				//oText.transform('s'+this.fZoom);
 				oNode.aElements.push(oText);
 				break;
 					
@@ -113,14 +124,15 @@ $(function()
 				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*this.fZoom, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
 				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*this.fZoom, yIcon + 18*this.fZoom, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
 				var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
+				oNode.text_attr['font-size'] = iFontSize * this.fZoom;
 				oText.attr(oNode.text_attr);
-				oText.transform('s'+this.fZoom);
+				//oText.transform('s'+this.fZoom);
 				var oBB = oText.getBBox();
 				var dy = iHeight/2*this.fZoom + oBB.height/2;
 				oText.remove();
 				oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label);
 				oText.attr(oNode.text_attr);
-				oText.transform('s'+this.fZoom);
+				//oText.transform('s'+this.fZoom);
 				oNode.aElements.push(oText);
 				oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}));
 				oText.toFront();
@@ -134,14 +146,15 @@ $(function()
 				}
 				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr));
 				var oText = this.oPaper.text( xPos, yPos, oNode.label);
+				oNode.text_attr['font-size'] = iFontSize * this.fZoom;
 				oText.attr(oNode.text_attr);
-				oText.transform('S'+this.fZoom);
+				//oText.transform('S'+this.fZoom);
 				var oBB = oText.getBBox();
 				var dy = iHeight/2*this.fZoom + oBB.height/2;
 				oText.remove();
 				oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
 				oText.attr(oNode.text_attr);
-				oText.transform('S'+this.fZoom);
+				//oText.transform('S'+this.fZoom);
 				oNode.aElements.push(oText);
 				oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}).toBack());
 				break;
@@ -201,6 +214,21 @@ $(function()
 			oNode.ty += (oNode.y - oNode.yOrig) * this.fZoom;
 			oNode.xOrig = oNode.x;
 			oNode.yOrig = oNode.y;
+			this._updateBBox();
+		},
+		_updateBBox: function()
+		{
+			this.options.xmin = 9999;
+			this.options.xmax = -9999;
+			this.options.ymin = 9999;
+			this.options.ymax = -9999;
+			for(var k in this.aNodes)
+			{
+				this.options.xmin = Math.min(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmin);
+				this.options.xmax = Math.max(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmax);
+				this.options.ymin = Math.min(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymin);
+				this.options.ymax = Math.max(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymax);
+			}
 		},
 		_get_edge_path: function(oEdge)
 		{
@@ -281,7 +309,7 @@ $(function()
 				break;
 				
 				case 'center':
-				this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
+				this.xOffset = -xmin * this.fZoom + (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
 				break;			
 			}
 			switch(this.options['vertical-align'])
@@ -295,15 +323,15 @@ $(function()
 				break;
 				
 				case 'middle':
-				this.yOffset = (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
+				this.yOffset = -ymin * this.fZoom + (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
 				break;			
-			}
-			
-			
+			}			
 		},
 		add_node: function(oNode)
 		{
 			oNode.aElements = [];
+			oNode.tx = 0;
+			oNode.ty = 0;
 			this.aNodes.push(oNode);
 		},
 		add_edge: function(oEdge)
@@ -325,15 +353,15 @@ $(function()
 		{
 			var sPopupMenuId = 'tk_graph'+this.element.attr('id');
 			var sHtml = '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
-			if (this.options.export_as_pdf_url != '')
+			if (this.options.export_as_pdf != null)
 			{
-				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">Export as PDF...</a></li>';			
+				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">'+this.options.export_as_pdf.label+'</a></li>';			
 			}
-			if (this.options.export_as_document_url != '')
+			if (this.options.export_as_document != null)
 			{
-				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_document">Export as document...</a></li>';
+				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_document">'+this.options.export_as_document.label+'</a></li>';
 			}
-			sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
+			//sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
 			sHtml += '</ul></li></ul></div>';
 			
 			this.element.before(sHtml);
@@ -377,7 +405,7 @@ $(function()
 	        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
 	        					me.show_group('relation_group_'+sGroupIndex);
 	        				},
-	        				items: { 'show': {name: 'Show group' } }
+	        				items: { 'show': {name: me.options.drill_down.label } }
 	        			};
 						break;
 						
@@ -387,16 +415,15 @@ $(function()
 		        		oResult = {
 	        				callback: function(key, options) {
 	        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
-	        					var sURL = me.options.drill_down_url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
+	        					var sURL = me.options.drill_down.url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
 	        					window.location.href = sURL;
 	        				},
-	        				items: { 'details': {name: 'Show Details' } }
+	        				items: { 'details': {name: me.options.drill_down.label } }
 	        			};
 		        		break;
 		        		
 		        		default:
 						oResult = false; // No context menu
-		        	
 		        	}
 		        	return oResult;
 		        }
@@ -410,11 +437,27 @@ $(function()
 			{
 				oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
 			}
-			var sHtmlForm = '<div id="PDFExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_as_pdf" target="_blank" action="'+this.options.export_as_pdf_url+'" method="post">';
+			var sHtmlForm = '<div id="PDFExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_as_pdf" target="_blank" action="'+this.options.export_as_pdf.url+'" method="post">';
 			sHtmlForm += '<input type="hidden" name="positions" value="">';
+			for(k in this.options.excluded)
+			{
+				sHtmlForm += '<input type="hidden" name="excluded[]" value="'+this.options.excluded[k]+'">';				
+			}
 			sHtmlForm += '<table>';
-			sHtmlForm += '<tr><td>Page format:</td><td><select name="p"><option value="A3">A3</option><option value="A4" selected>A4</option><option value="Letter">Letter</option></select></td></tr>';
-			sHtmlForm += '<tr><td>Page orientation:</td><td><select name="o"><option value="L" selected>Landscape</option><option value="P">Portrait</select></td></tr>';
+			sHtmlForm += '<tr><td>'+this.options.page_format.label+'</td><td><select name="p">';
+			for(k in this.options.page_format.values)
+			{
+				var sSelected = (k == this.options.page_format['default']) ? ' selected' : '';
+				sHtmlForm += '<option value="'+k+'"'+sSelected+'>'+this.options.page_format.values[k]+'</option>';
+			}
+			sHtmlForm += '</select></td></tr>';
+			sHtmlForm += '<tr><td>'+this.options.page_orientation.label+'</td><td><select name="o">';
+			for(k in this.options.page_orientation.values)
+			{
+				var sSelected = (k == this.options.page_orientation['default']) ? ' selected' : '';
+				sHtmlForm += '<option value="'+k+'"'+sSelected+'>'+this.options.page_orientation.values[k]+'</option>';
+			}
+			sHtmlForm += '</select></td></tr>';
 			sHtmlForm += '<table>';
 			sHtmlForm += '</form></div>';
 			
@@ -423,21 +466,56 @@ $(function()
 			var me = this;
 			$('#PDFExportDlg'+this.element.attr('id')).dialog({
 				modal: true,
-				title: 'PDF format options',
+				title: this.options.labels.export_pdf_title,
+				close: function() { $(this).remove(); },
 				buttons: [
-				          {text: 'Cancel', click: function() { $(this).dialog('close');} },
-				          {text: 'Export', click: function() { $('#graph_'+me.element.attr('id')+'_export_as_pdf').submit(); $(this).dialog('close');} },
+				          {text: this.options.labels['cancel'], click: function() { $(this).dialog('close');} },
+				          {text: this.options.labels['export'], click: function() { $('#graph_'+me.element.attr('id')+'_export_as_pdf').submit(); $(this).dialog('close');} },
 				]
 			});
 			//$('#graph_'+this.element.attr('id')+'_export_as_pdf').submit();
 		},
+		_on_resize: function()
+		{
+			this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
+			this.auto_scale();
+			this.oPaper.setSize(this.element.width(), this.element.height());
+			this.draw();
+		},
+		load: function(oData)
+		{
+			this.aNodes = [];
+			this.aEdges = [];
+			for(k in oData.nodes)
+			{
+				this.add_node(oData.nodes[k]);
+			}
+			for(k in oData.edges)
+			{
+				this.add_edge(oData.edges[k]);
+			}
+			this._updateBBox();
+			this.auto_scale();
+			this.oPaper.setSize(this.element.width(), this.element.height());
+			this.draw();
+		},
+		load_from_url: function(sUrl)
+		{
+			this.options.load_from_url = sUrl;
+			var me = this;
+			this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
+			this.oPaper.rect(0, 0, this.element.width(), this.element.height()).attr({fill: '#000', opacity: 0.4, 'stroke-width': 0});
+			$.post(sUrl, {excluded: this.options.excluded }, function(data) {
+				me.load(data);
+			}, 'json');
+		},
 		export_as_document: function()
 		{
 			alert('Export as document: not yet implemented');
 		},
 		reload: function()
 		{
-			alert('Reload: not yet implemented');
+			this.load_from_url(this.options.load_from_url);
 		}
 	});	
 });

+ 34 - 4
pages/UI.php

@@ -311,14 +311,43 @@ EOF
 		$sExportAsPdfURL = '';
 		if (extension_loaded('gd'))
 		{
-			$sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($bDirectionDown ? 'down' : 'up').'&class='.$sClass.'&id='.$id;
+			$sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($bDirectionDown ? 'down' : 'up').'&class='.$sClass.'&id='.$id.'&g='.$iGroupingThreshold;
 		}
 		$oAppcontext = new ApplicationContext();
 		$sContext = $oAppContext->GetForLink();
 		$sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
 		$sExportAsDocumentURL = '';
+		$sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($bDirectionDown ? 'down' : 'up').'&class='.$sClass.'&id='.$id.'&g='.$iGroupingThreshold;
 		
-		$oDisplayGraph->RenderAsRaphael($oP, null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL);
+		$sId = 'graph';
+		$oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
+		$aParams = array(
+			'source_url' => $sLoadFromURL,
+			'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
+			//'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
+			'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
+			'labels' => array(
+				'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
+				'export' => Dict::S('UI:Button:Export'),
+				'cancel' => Dict::S('UI:Button:Cancel'),
+			),
+			'page_format' => array(
+				'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
+				'values' => array(
+					'A3' => Dict::S('UI:PageFormat_A3'),
+					'A4' => Dict::S('UI:PageFormat_A4'),
+					'Letter' => Dict::S('UI:PageFormat_Letter'),
+				),
+			),
+			'page_orientation' => array(
+				'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
+				'values' => array(
+					'P' => Dict::S('UI:PageOrientation_Portrait'),
+					'L' => Dict::S('UI:PageOrientation_Landscape'),
+				),
+			),
+		);
+		$oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");");
 	}
 	catch(Exception $e)
 	{
@@ -334,12 +363,13 @@ EOF
 		{
 			var aExcluded = [];
 			$('input[name^=excluded]').each( function() {
-				if (!$(this).attr('checked'))
+				if (!$(this).prop('checked'))
 				{
 					aExcluded.push($(this).val());
 				}
 			} );
-			alert('Not yet implemented');
+			$('#graph').simple_graph('option', {excluded: aExcluded});
+			$('#graph').simple_graph('reload');
 		}
 		catch(err)
 		{

+ 68 - 2
pages/ajax.render.php

@@ -1740,6 +1740,7 @@ EOF
 		$sPageOrientation = utils::ReadParam('o', 'L');
 		$sTitle = utils::ReadParam('title', '', false, 'raw_data');
 		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
 		$aPositions = null;
 		if ($sPositions != null)
 		{
@@ -1758,7 +1759,20 @@ EOF
 			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
 		}
 		
-
+		// Remove excluded classes from the graph
+		if (count($aExcluded) > 0)
+		{
+			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+			foreach($oIterator as $oNode)
+			{
+				$oObj = $oNode->GetProperty('object');
+				if ($oObj && in_array(get_class($oObj), $aExcluded))
+				{
+					$oRelGraph->FilterNode($oNode);
+				}
+			}
+		}
+		
 		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
 		$oGraph->InitFromGraphviz();
 		if ($aPositions != null)
@@ -1771,7 +1785,59 @@ EOF
 		$oPage->SetContentDisposition('inline', 'iTop.pdf');
 		break;
 		
-
+		case 'relation_json':
+		require_once(APPROOT.'core/simplegraph.class.inc.php');
+		require_once(APPROOT.'core/relationgraph.class.inc.php');
+		require_once(APPROOT.'core/displayablegraph.class.inc.php');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$id = utils::ReadParam('id', 0);
+		$sRelation = utils::ReadParam('relation', 'impact');
+		$sDirection = utils::ReadParam('direction', 'down');
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+		$sPositions = utils::ReadParam('positions', null, false, 'raw_data');
+		$aExcluded = utils::ReadParam('excluded', array(), false, 'raw_data');
+		$aPositions = null;
+		if ($sPositions != null)
+		{
+			$aPositions = json_decode($sPositions, true);
+		}
+		
+		$oObj = MetaModel::GetObject($sClass, $id);
+		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+		$aSourceObjects = array($oObj);
+		if ($sDirection == 'up')
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+		}
+		else
+		{
+			$oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+		}
+		
+		// Remove excluded classes from the graph
+		if (count($aExcluded) > 0)
+		{
+			$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+			foreach($oIterator as $oNode)
+			{
+				$oObj = $oNode->GetProperty('object');
+				if ($oObj && in_array(get_class($oObj), $aExcluded))
+				{
+					$oRelGraph->FilterNode($oNode);
+				}
+			}
+		}
+		
+		$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		$oGraph->InitFromGraphviz();
+		if ($aPositions != null)
+		{
+			$oGraph->UpdatePositions($aPositions);
+		}
+		$oPage->add($oGraph->GetAsJSON());
+		$oPage->SetContentType('application/json');
+		break;
+		
 		default:
 		$oPage->p("Invalid query.");
 	}