// jQuery UI style "widget" for displaying a graph

////////////////////////////////////////////////////////////////////////////////
//
// graph
//
$(function()
{
	// the widget definition, where "itop" is the namespace,
	// "dashboard" the widget name
	$.widget( "itop.simple_graph",
	{
		// default options
		options:
		{
			align: 'center',
			'vertical-align': 'middle',
			source_url: null,
			sources: {},
			excluded: {},
			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',
				title: 'Document Title',
				include_list: 'Include the list of objects',
				comments: 'Comments',
				grouping_threshold: 'Grouping Threshold',
				additional_context_info: 'Additional Context Info',
				refresh: 'Refresh',
				check_all: 'Check All',
				uncheck_all: 'Uncheck All',
				none_selected: 'None',
				nb_selected: '# selected',
				zoom: 'Zoom',
				loading: 'Loading...'
			},
			export_as_document: null,
			drill_down: null,
			grouping_threshold: 10,
			excluded_classes: [],
			attachment_obj_class: null,
			attachment_obj_key: null,
			additional_contexts: [],
			context_key: ''
		},
	
		// the constructor
		_create: function()
		{
			var me = this;
			this.aNodes = [];
			this.aEdges = [];
			this.fZoom = 1.0;
			this.xOffset = 0;
			this.yOffset = 0;
			this.xPan = 0;
			this.yPan = 0;
			this.iTextHeight = 12;
			this.fSliderZoom = 1.0;
			this.bInUpdateSliderZoom = false;
			this.bRedrawNeeded = false;
			
			this.oPaper = Raphael(this.element.get(0), 16*this.element.width(), 16*this.element.height());

			this.element
			.addClass('panel-resized')
			.addClass('itop-simple-graph')
			.addClass('graph');
			
			this._create_toolkit_menu();
			this._build_context_menus();
			this.sTabId = null;
			var jTabPanel = this.element.closest('.ui-tabs-panel');
			if (jTabPanel.length > 0)
			{
				// We are inside a tab, find out which one and hook its activation
				this.sTabId = jTabPanel.attr('id');
				var jTabs = this.element.closest('.ui-tabs');
				jTabs.on( "tabsactivate", function( event, ui ) {
					me._on_tabs_activate(ui);
				});					
			}
			$(window).bind('resized', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } );
			$('#dh_flash').bind('toggle_complete', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } );
			this.element.bind('mousewheel', function(event, delta, deltaX, deltaY) {
			    return me._on_mousewheel(event, delta, deltaX, deltaY);
			});
			if (this.options.source_url != null)
			{
				this.load_from_url(this.options.source_url);
			}
		},
		// called when created, and later when changing options
		_refresh: function()
		{
			this.draw();
		},
		// events bound via _bind are removed automatically
		// revert other modifications here
		_destroy: function()
		{
			var sId = this.element.attr('id');
			this.element
			.removeClass('itop-simple-graph')
			.removeClass('graph');
			
			$('#tk_graph'+sId).remove();
			$('#graph_'+sId+'_export_as_pdf').remove();
			
		},
		// _setOptions is called with a hash of all options that are changing
		_setOptions: function()
		{
			this._superApply(arguments);
		},
		// _setOption is called for each individual option that is changing
		_setOption: function( key, value )
		{
			this._superApply(arguments);
		},
		draw: function()
		{
			this._updateBBox();
			this.auto_scale();
			this.oPaper.clear();
			this._reset
			this.oPaper.setViewBox(this.xPan, this.yPan, this.element.width(),  this.element.height(), false);
			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]);
			}
			var me = this;
			this.oBackground = this.oPaper
			.rect(-10000, -10000, 20000, 20000)
			.attr({fill: '#fff', opacity: 0, cursor: 'move'})
			.toBack()
			.drag(function(dx, dy, x, y, event) { me._on_background_move(dx, dy, x, y, event); }, function(x, y, event) { me._on_background_drag_start(x, y, event); }, function (event) { me._on_background_drag_end(event); });
			this._make_tooltips();
		},
		_draw_node: function(oNode)
		{
			var iWidth = oNode.width;
			var iHeight = 32;
			var iFontSize = 10;
			var fTotalZoom = this.fZoom * this.fSliderZoom;
			var xPos = Math.round(oNode.x * fTotalZoom + this.xOffset);
			var yPos = Math.round(oNode.y * fTotalZoom + this.yOffset);
			oNode.tx = 0;
			oNode.ty = 0;
			switch(oNode.shape)
			{
				case 'disc':
				oScaledAttr = {};
				for(k in oNode.disc_attr)
				{
					value = oNode.disc_attr[k]
					switch(k)
					{
						// Scalable attributes
						case 'stroke-width':
						value = value * fTotalZoom;
						break;
					}
					oScaledAttr[k] = value;
				}
				oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr(oScaledAttr));
				var oText = this.oPaper.text(xPos, yPos, oNode.label);
				oNode.text_attr['font-size'] = iFontSize * fTotalZoom;
				oText.attr(oNode.text_attr);
				//oText.transform('s'+this.fZoom);
				oNode.aElements.push(oText);
				break;
					
				case 'group':
				oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr({fill: '#fff', 'stroke-width':0}));
				oScaledAttr = {};
				for(k in oNode.disc_attr)
				{
					value = oNode.disc_attr[k]
					switch(k)
					{
						// Scalable attributes
						case 'stroke-width':
						value = value * fTotalZoom;
						break;
					}
					oScaledAttr[k] = value;
				}
				oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*fTotalZoom / 2).attr(oScaledAttr));
				var xIcon = xPos - 18 * fTotalZoom;
				var yIcon = yPos - 18 * fTotalZoom;
				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon, yIcon, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr));
				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*fTotalZoom, yIcon, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr));
				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*fTotalZoom, yIcon + 18*fTotalZoom, 16*fTotalZoom, 16*fTotalZoom).attr(oNode.icon_attr));
				var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
				oNode.text_attr['font-size'] = iFontSize * fTotalZoom;
				oText.attr(oNode.text_attr);
				//oText.transform('s'+this.fZoom);
				var oBB = oText.getBBox();
				var dy = iHeight/2*fTotalZoom + 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);
				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();
				break;
					
				case 'icon':
				if(Raphael.svg)
				{
					// the colorShift plugin works only in SVG
					oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * fTotalZoom/2, yPos - iHeight * fTotalZoom/2, iWidth*fTotalZoom, iHeight*fTotalZoom).colorShift('#fff', 1));					
				}
				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * fTotalZoom/2, yPos - iHeight * fTotalZoom/2, iWidth*fTotalZoom, iHeight*fTotalZoom).attr(oNode.icon_attr));
				
				var idx = 0;
				for(var i in oNode.context_icons)
				{
					var sgn = 2*(idx % 2) -1; // Suite: -1, 1, -1, 1, -1, 1, -1, etc.
					var coef = Math.floor((1+idx)/2) * sgn; // Suite: 0, 1, -1, 2, -2, 3, -3, etc.
					var alpha = coef*Math.PI/4 - Math.PI/2;						
					var x = xPos + Math.cos(alpha) * 1.25*iWidth * fTotalZoom / 2;
					var y = yPos + Math.sin(alpha) * 1.25*iWidth * fTotalZoom / 2;
					var l = iWidth/3 * fTotalZoom;
					oNode.aElements.push(this.oPaper.image(oNode.context_icons[i], x - l/2, y - l/2, l , l).attr(oNode.icon_attr));
					idx++;
				}
				var oText = this.oPaper.text( xPos, yPos, oNode.label);
				oNode.text_attr['font-size'] = iFontSize * fTotalZoom;
				oText.attr(oNode.text_attr);
				//oText.transform('S'+fTotalZoom);
				var oBB = oText.getBBox();
				var dy = iHeight/2*fTotalZoom + oBB.height/2;
				oText.remove();
				oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
				oText.attr(oNode.text_attr);
				//oText.transform('S'+fTotalZoom);
				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;
			}
			if (oNode.source)
			{
				oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*fTotalZoom / 2).attr({stroke: '#c33', 'stroke-width': 3*fTotalZoom }).toBack());
			}
			if (oNode.sink)
			{
				oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*fTotalZoom / 2).attr({stroke: '#33c', 'stroke-width': 3*fTotalZoom }).toBack());
			}
			
			var me = this;
			for(k in oNode.aElements)
			{
				var sNodeId = oNode.id;
				$(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget');
				oNode.aElements[k].drag(
					function(dx, dy, x, y, event) {
						clearTimeout($(this.node).data('openTimeoutId'));
						me._move(sNodeId, dx, dy, x, y, event);
					},
					function(x, y, event) { 
						me._drag_start(sNodeId, x, y, event);
					},
					function (event) {
						me._drag_end(sNodeId, event);
					}
				);
			}
		},
		_move: function(sNodeId, dx, dy, x, y, event)
		{
			var fTotalZoom = this.fZoom * this.fSliderZoom;
			var origDx = dx / fTotalZoom;
			var origDy = dy / fTotalZoom;
			
			var oNode = this._find_node(sNodeId);
			oNode.x = oNode.xOrig + origDx;
			oNode.y = oNode.yOrig + origDy;
			
			for(k in oNode.aElements)
			{
				oNode.aElements[k].transform('t'+(oNode.tx + dx)+', '+(oNode.ty + dy));
				
				for(j in this.aEdges)
				{
					var oEdge = this.aEdges[j];
					if ((oEdge.source_node_id == sNodeId) || (oEdge.sink_node_id == sNodeId))
					{
						var sPath = this._get_edge_path(oEdge);
						oEdge.aElements[0].attr({path: sPath});
					}
				}
			}
		},
		_drag_start: function(sNodeId, x, y, event)
		{
			var oNode = this._find_node(sNodeId);
			oNode.xOrig = oNode.x;
			oNode.yOrig = oNode.y;
			
		},
		_drag_end: function(sNodeId, event)
		{
			var fTotalZoom = this.fZoom * this.fSliderZoom;
			var oNode = this._find_node(sNodeId);
			oNode.tx += (oNode.x - oNode.xOrig) * fTotalZoom;
			oNode.ty += (oNode.y - oNode.yOrig) * fTotalZoom;
			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.aNodes[k].width/2, this.options.xmin);
				this.options.xmax = Math.max(this.aNodes[k].x + this.aNodes[k].tx + this.aNodes[k].width/2, this.options.xmax);
				this.options.ymin = Math.min(this.aNodes[k].y + this.aNodes[k].ty - this.aNodes[k].width/2, this.options.ymin);
				this.options.ymax = Math.max(this.aNodes[k].y + this.aNodes[k].ty + this.aNodes[k].width/2, this.options.ymax);
			}
		},
		_get_edge_path: function(oEdge)
		{
			var fTotalZoom = this.fZoom * this.fSliderZoom;
			var oStart = this._find_node(oEdge.source_node_id);
			var oEnd = this._find_node(oEdge.sink_node_id);
			var iArrowSize = 5;
			
			if ((oStart == null) || (oEnd == null)) return '';
			
			var xStart = Math.round(oStart.x * fTotalZoom + this.xOffset);
			var yStart = Math.round(oStart.y * fTotalZoom + this.yOffset);
			var xEnd = Math.round(oEnd.x * fTotalZoom + this.xOffset);
			var yEnd = Math.round(oEnd.y  * fTotalZoom + this.yOffset);

			var sPath = Raphael.format('M{0},{1}L{2},{3}', xStart, yStart, xEnd, yEnd);
			var vx = (xEnd - xStart);
			var vy = (yEnd - yStart);
			var l = Math.sqrt(vx*vx+vy*vy);
			vx = vx / l;
			vy = vy / l;
			var ux = -vy;
			var uy = vx;
			var lPos = Math.max(l/2, l - 40*fTotalZoom);
			var xArrow = xStart + vx * lPos;
			var yArrow = yStart + vy * lPos;
			sPath += Raphael.format('M{0},{1}l{2},{3}M{4},{5}l{6},{7}', xArrow, yArrow, fTotalZoom * iArrowSize *(-vx + ux),  fTotalZoom * iArrowSize *(-vy + uy), xArrow, yArrow, fTotalZoom * iArrowSize *(-vx - ux),  fTotalZoom * iArrowSize *(-vy - uy));
			return sPath;
		},
		_draw_edge: function(oEdge)
		{
			var fTotalZoom = this.fZoom * this.fSliderZoom;
			var fStrokeSize = Math.max(1, 2 * fTotalZoom);			
			var sPath = this._get_edge_path(oEdge);
			var oAttr = $.extend(oEdge.attr);
			oAttr['stroke-linecap'] = 'round';
			oAttr['stroke-width'] = fStrokeSize;		
			oEdge.aElements.push(this.oPaper.path(sPath).attr(oAttr).toBack());
		},
		_find_node: function(sId)
		{
			for(var k in this.aNodes)
			{
				if (this.aNodes[k].id == sId) return this.aNodes[k];
			}
			return null;
		},
		adjust_height: function()
		{
			var maxHeight = this.element.parent().height();
			// Compute the available height
			var element = this.element;
			this.element.parent().children().each(function() {
				if($(this).is(':visible') && !$(this).hasClass('graph') && ($(this).attr('id') != element.attr('id')))
				{
					maxHeight = maxHeight - $(this).height();
				}
			});
			
			this.element.height(maxHeight - 13);
			this.oPaper.setSize(this.element.width(), this.element.height());
		},
		auto_scale: function()
		{
			var fMaxZoom = 1.5;
			this.adjust_height();
			
			iMargin = 10;
			xmin = this.options.xmin - iMargin;
			xmax = this.options.xmax + iMargin;
			ymin = this.options.ymin - iMargin;
			ymax = this.options.ymax + iMargin;
			var xScale = this.element.width() / (xmax - xmin);
			var yScale = this.element.height() / (ymax - ymin + this.iTextHeight);

			this.fZoom = Math.min(xScale, yScale, fMaxZoom);
			switch(this.options.align)
			{
				case 'left':
				this.xOffset = -xmin * this.fZoom;
				break;
				
				case 'right':
				this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom);
				break;
				
				case 'center':
				this.xOffset = -xmin * this.fZoom + (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
				break;			
			}
			switch(this.options['vertical-align'])
			{
				case 'top':
				this.yOffset = -ymin * this.fZoom;
				break;
				
				case 'bottom':
				this.yOffset = this.element.height() - (ymax + this.iTextHeight) * this.fZoom;
				break;
				
				case 'middle':
				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)
		{
			oEdge.aElements = [];
			this.aEdges.push(oEdge);
		},
		show_group: function(sGroupId)
		{
			this._close_all_tooltips();
			// Activate the 3rd tab
			this.element.closest('.ui-tabs').tabs("option", "active", 2);
			// Scroll into view the group
			if ($('#'+sGroupId).length > 0)
			{
				$('#'+sGroupId)[0].scrollIntoView();				
			}
		},
		_create_toolkit_menu: function()
		{
			var sPopupMenuId = 'tk_graph'+this.element.attr('id');
			var sHtml = '<div class="graph_config">';
			var sId = this.element.attr('id');
			sHtml += this.options.labels.grouping_threshold+'&nbsp;<input type="text" name="g" value="'+this.options.grouping_threshold+'" id="'+sId+'_grouping_threshold" size="2">';
			if (this.options.additional_contexts.length > 0)
			{
				sHtml += '&nbsp;'+this.options.labels.additional_context_info+' <select id="'+sId+'_contexts" name="contexts" class="multiselect" multiple size="1">';
				for(var k in this.options.additional_contexts)
				{
					sSelected = (this.options.additional_contexts[k]['default']) ? 'selected' : '';
					sHtml += '<option value="'+k+'" '+sSelected+'>'+this.options.additional_contexts[k].label+'</option>';
				}
				sHtml += '</select>'
			}
			sHtml += '&nbsp;<button type="button" id="'+sId+'_refresh_btn">'+this.options.labels.refresh+'</button>';
			sHtml += '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><i class="fas fa-tools"></i><i class="fas fa-caret-down"></i><ul>';
			if (this.options.export_as_pdf != null)
			{
				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">'+this.options.export_as_pdf.label+'</a></li>';			
			}
			if (this.options.export_as_attachment != null)
			{
				sHtml += '<li><a href="#" id="'+sPopupMenuId+'_attachment">'+this.options.export_as_attachment.label+'</a></li>';
			}
			//sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
			sHtml += '</ul></li></ul></div>';
			sHtml += '<span class="graph_zoom"><span>'+this.options.labels.zoom+'</span>';
			sHtml += '<div id="'+sId+'_zoom_minus" class="graph_zoom_minus ui-icon ui-icon-circle-minus"></div>';
			sHtml += '<div id="'+sId+'_zoom" class="graph_zoom_slider"></div>';
			sHtml += '<div id="'+sId+'_zoom_plus" class="graph_zoom_plus ui-icon ui-icon-circle-plus"></div>';
			sHtml += '</span>';
			sHtml += '</div>';

			
			this.element.before(sHtml);
			$('#'+sPopupMenuId+'>ul').popupmenu();
			
			
			var me = this;
			$('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
			$('#'+sPopupMenuId+'_attachment').click(function() { me.export_as_attachment(); });
			$('#'+sId+'_grouping_threshold').spinner({ min: 2});
			$('#'+sId+'_zoom').slider({ min: 0, max: 5, value: 1, step: 0.25, change: function() { me._on_zoom_change( $(this).slider('value')); } });
			$('#'+sId+'_zoom_plus').click(function() { $('#'+sId+'_zoom').slider('value', 0.25 + $('#'+sId+'_zoom').slider('value')); return false; });
			$('#'+sId+'_zoom_minus').click(function() { $('#'+sId+'_zoom').slider('value', $('#'+sId+'_zoom').slider('value') - 0.25); return false; });
			$('#'+sId+'_contexts').multiselect({header: true, checkAllText: this.options.labels.check_all, uncheckAllText: this.options.labels.uncheck_all, noneSelectedText: this.options.labels.none_selected, selectedText: this.options.labels.nb_selected, selectedList: 1});
			$('#'+sId+'_refresh_btn').button().click(function() { me.reload(); });
		},
		_build_context_menus: function()
		{
			var sId = this.element.attr('id');
			var me = this;
			
			$.contextMenu({
			    selector: '#'+sId+' .popupMenuTarget',  
		        build: function(trigger, e) {
		            // this callback is executed every time the menu is to be shown
		            // its results are destroyed every time the menu is hidden
		            // e is the original contextmenu event, containing e.pageX and e.pageY (amongst other data)
		        	var sType = trigger.attr('data-type');
		        	var sNodeId = trigger.attr('data-id');
		        	var oNode = me._find_node(sNodeId);
		        	clearTimeout(trigger.data('openTimeoutId'));
		        	
		        	/*
		        	var sObjName = trigger.attr('data-class');
		        	var sIndex = trigger.attr('data-index');
		        	var originalEvent = e;
		        	var bHasItems = false;
		        	*/
		        	var oResult = {callback: null, items: {}};
		        	switch(sType)
		        	{
		        		case 'group':
		        		var sGroupIndex = oNode.group_index;
		        		if( $('#relation_group_'+sGroupIndex).length > 0)
		        		{
			        		oResult = {
			        				callback: function(key, options) {
			        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
			        					me.show_group('relation_group_'+sGroupIndex);
			        				},
			        				items: { 'show': {name: me.options.drill_down.label } }
			        			};		        			
		        		}
						break;
						
		        		case 'icon':
			        	var sObjClass = oNode.obj_class;
		        		var sObjKey = oNode.obj_key;
		        		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);
	        					window.location.href = sURL;
	        				},
	        				items: { 'details': {name: me.options.drill_down.label } }
	        			};
		        		break;
		        		
		        		default:
						oResult = false; // No context menu
		        	}
		        	return oResult;
		        }
			});
			
		},
		export_as_pdf: function()
		{
			this._export_dlg(this.options.labels.export_pdf_title, this.options.export_as_pdf.url, 'download_pdf');
		},
		_export_dlg: function(sTitle, sSubmitUrl, sOperation)
		{
			var sId = this.element.attr('id');
			var me = this;
			var oPositions = {};
			for(k in this.aNodes)
			{
				oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
			}
			var sHtmlForm = '<div id="GraphExportDlg'+this.element.attr('id')+'"><form id="graph_'+this.element.attr('id')+'_export_dlg" target="_blank" action="'+sSubmitUrl+'" method="post">';
			sHtmlForm += '<input type="hidden" name="g" value="'+this.options.grouping_threshold+'">';
			sHtmlForm += '<input type="hidden" name="context_key" value="'+this.options.context_key+'">';
			$('#'+sId+'_contexts').multiselect('getChecked').each(function() {
				sHtmlForm += '<input type="hidden" name="contexts['+$(this).val()+']" value="'+me.options.additional_contexts[$(this).val()].oql+'">';				
			});

			sHtmlForm += '<input type="hidden" name="positions" value="">';
			for(k in this.options.excluded_classes)
			{
				sHtmlForm += '<input type="hidden" name="excluded_classes[]" value="'+this.options.excluded_classes[k]+'">';				
			}
			for(var k1 in this.options.sources)
			{
				for(var k2 in this.options.sources[k1])
				{
					sHtmlForm += '<input type="hidden" name="sources['+k1+'][]" value="'+this.options.sources[k1][k2]+'">';									
				}
			}
			for(var k1 in this.options.excluded)
			{
				for(var k2 in this.options.excluded[k1])
				{
					sHtmlForm += '<input type="hidden" name="excluded['+k1+'][]" value="'+this.options.excluded[k1][k2]+'">';									
				}
			}
			if (sOperation == 'attachment')
			{
				sHtmlForm += '<input type="hidden" name="obj_class" value="'+this.options.export_as_attachment.obj_class+'">';									
				sHtmlForm += '<input type="hidden" name="obj_key" value="'+this.options.export_as_attachment.obj_key+'">';								
			}
			sHtmlForm += '<table>';
			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 += '<tr><td>'+this.options.labels.title+'</td><td><input name="title" value="'+this.options.labels.untitled+'" style="width: 20em;"/></td></tr>';
			sHtmlForm += '<tr><td>'+this.options.labels.comments+'</td><td><textarea style="width: 20em; height:5em;" name="comments"/></textarea></td></tr>';
			sHtmlForm += '<tr><td colspan=2><input type="checkbox" checked id="include_list_checkbox" name="include_list" value="1"><label for="include_list_checkbox">&nbsp;'+this.options.labels.include_list+'</label></td></tr>';
			sHtmlForm += '<table>';
			sHtmlForm += '</form></div>';
			
			$('body').append(sHtmlForm);
			$('#graph_'+this.element.attr('id')+'_export_dlg input[name="positions"]').val(JSON.stringify(oPositions));
			var me = this;
			if (sOperation == 'attachment')
			{
				$('#GraphExportDlg'+this.element.attr('id')+' form').submit(function() { return me._on_export_as_attachment(); });
			}
			$('#GraphExportDlg'+this.element.attr('id')).dialog({
				width: 'auto',
				modal: true,
				title: sTitle,
				close: function() { $(this).remove(); },
				buttons: [
				          {text: this.options.labels['cancel'], click: function() { $(this).dialog('close');} },
				          {text: this.options.labels['export'], click: function() { $('#graph_'+me.element.attr('id')+'_export_dlg').submit(); $(this).dialog('close');} },
				]
			});			
		},
		_on_zoom_change: function(sliderValue)
		{
			if(!this.bInUpdateSliderZoom)
			{
				var Z0 = this.fSliderZoom;
				var X = this.xOffset - this.element.width()/2;
				var Y = this.yOffset - this.element.height()/2; 

				this.fSliderZoom = Math.pow(2 , (sliderValue - 1));
				
				var Z1 = this.fSliderZoom = Math.pow(2 , (sliderValue - 1));
				var dx = X * (1 - Z1/Z0);
				var dy = Y * (1 - Z1/Z0);
				this.xPan += dx;
				this.yPan += dy;
				this._close_all_tooltips();
				this.oPaper.setViewBox(this.xPan, this.yPan, this.element.width(),  this.element.height(), false);
				this.draw();				
			}
		},
		_on_mousewheel: function(event, delta, deltaX, deltaY)
		{
			var fStep = 0.25*delta;
			var sId = this.element.attr('id');
			$('#'+sId+'_zoom').slider('value', fStep + $('#'+sId+'_zoom').slider('value'));
		},
		_on_resize: function()
		{
			this.element.closest('.ui-tabs').tabs({ heightStyle: "content" });
			this.auto_scale();
			this._close_all_tooltips();
			this.draw();
		},
		_on_tabs_activate: function(ui)
		{
			if (ui.newPanel.selector == ('#'+this.sTabId))
			{
				if (this.bRedrawNeeded)
				{
					this._updateBBox();
					this.auto_scale();
					this.oPaper.setSize(this.element.width(), this.element.height());
					this._reset_pan_and_zoom();
					this.draw();
					bRedrawNeeded = false;
				}
			}
		},
		load: function(oData)
		{
			var me = this;
			var sId = this.element.attr('id');
			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]);
			}
			if (oData.groups)
			{
				this.refresh_groups(oData.groups);
			}
			if (oData.lists)
			{
				this.refresh_lists(oData.lists);
			}
			if (this.element.is(':visible'))
			{
				this._updateBBox();
				this.auto_scale();
				this._reset_pan_and_zoom();
				this.draw();
			}
			else
			{
				this.bRedrawNeeded = true;
			}
		},
		refresh_groups: function(aGroups)
		{
			if ($('#impacted_groups').length > 0)
			{
				
				// The "Groups" tab is present, refresh it
				if (aGroups.length == 0)
				{
					this.element.closest('.ui-tabs').tabs("disable", 2);
					$('#impacted_groups').html('');
				}
				else
				{
					this.element.closest('.ui-tabs').tabs("enable", 2);
					$('#impacted_groups').html('<img src="../images/indicator.gif">');
					var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
					$.post(sUrl, { operation: 'relation_groups', groups: aGroups }, function(data) {
						$('#impacted_groups').html(data);
					});
				}
			}
		},
		refresh_lists: function(aLists)
		{
			if ($('#impacted_objects_lists').length > 0)
			{
				// The "Lists" tab is present, refresh it
				if (aLists.length == 0)
				{
					$('#impacted_objects_lists').html('');
				}
				else
				{
					$('#impacted_objects_lists').html('<img src="../images/indicator.gif">');
					var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
					$.post(sUrl, { operation: 'relation_lists', lists: aLists }, function(data) {
						$('#impacted_objects_lists').html(data);
					});
				}
			}
		},
		_reset_pan_and_zoom: function()
		{
			this.xPan = 0;
			this.yPan = 0;
			var sId = this.element.attr('id');
			this.bInUpdateSliderZoom = true;
			$('#'+sId+'_zoom').slider('value', 1);
			this.fSliderZoom = 1.0;
			this.bInUpdateSliderZoom = false;
			this.oPaper.setViewBox(this.xPan, this.yPan, this.element.width(),  this.element.height(), false);
		},
		load_from_url: function(sUrl)
		{
			this.options.load_from_url = sUrl;
			var me = this;
			var sId = this.element.attr('id');
			this.options.grouping_threshold = $('#'+sId+'_grouping_threshold').val();
			if (this.options.grouping_threshold < 2)
			{
				this.options.grouping_threshold = 2;
				$('#'+sId+'_grouping_threshold').val(this.options.grouping_threshold);
			}
			var aContexts = [];
			$('#'+sId+'_contexts').multiselect('getChecked').each(function() { aContexts[$(this).val()] = me.options.additional_contexts[$(this).val()].oql; });
			// Don't set the height on the tabs widget, only on the current tab
			// Adjust the height of the graph to the window size
			var sTabHeight = parseInt($(window).height() - this.element.parent().offset().top - 50);
			this.element.closest('.ui-tabs-panel').css({ height: sTabHeight + "px" });

			this.adjust_height();
			this._close_all_tooltips();
			this.oPaper.rect(this.xPan, this.yPan, this.element.width(), this.element.height()).attr({fill: '#000', opacity: 0.4, 'stroke-width': 0});
			this.oPaper.rect(this.xPan + this.element.width()/2 - 100, this.yPan + this.element.height()/2 - 10, 200, 20)
			.attr({fill: 'url(../setup/orange-progress.gif)', stroke: '#000', 'stroke-width': 1});
			this.oPaper.text(this.xPan + this.element.width()/2, this.yPan + this.element.height()/2 - 20, this.options.labels.loading);			
			
			$('#'+sId+'_refresh_btn').button('disable'); 
			$.post(sUrl, {excluded_classes: this.options.excluded_classes, g: this.options.grouping_threshold, sources: this.options.sources, excluded: this.options.excluded, contexts: aContexts, context_key: this.options.context_key }, function(data) {
				me.load(data);
				$('#'+sId+'_refresh_btn').button('enable');
			}, 'json');
		},
		export_as_attachment: function()
		{
			this._export_dlg(this.options.labels.export_as_attachment_title, this.options.export_as_attachment.url, 'attachment');
		},
		_on_export_as_attachment: function()
		{
			var oParams = {};
			var oPositions = {};
			var jForm = $('#GraphExportDlg'+this.element.attr('id')+' form');
			for(k in this.aNodes)
			{
				oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
			}
			oParams.positions = JSON.stringify(oPositions);
			oParams.sources = this.options.sources;
			oParams.excluded_classes = this.options.excluded_classes;
			oParams.title = jForm.find(':input[name="title"]').val();
			oParams.comments = jForm.find(':input[name="comments"]').val();
			oParams.include_list = jForm.find(':input[name="include_list"]:checked').length;
			oParams.o = jForm.find(':input[name="o"]').val();
			oParams.p = jForm.find(':input[name="p"]').val();
			oParams.obj_class = this.options.export_as_attachment.obj_class;
			oParams.obj_key = this.options.export_as_attachment.obj_key;
			oParams.contexts = [];
			var me = this;
			$('#'+this.element.attr('id')+'_contexts').multiselect('getChecked').each(function() {
				oParams.contexts[$(this).val()] = me.options.additional_contexts[$(this).val()].oql;				
			});
			oParams.context_key = this.options.context_key;
			var sUrl = jForm.attr('action');
			var sTitle = oParams.title;
			var jPanel = $('#attachments').closest('.ui-tabs-panel');
			var jTab = null;
			var sTabText = null;
			if (jPanel.length  > 0)
			{
				var sTabId = jPanel.attr('id');
				jTab = $('li[aria-controls='+sTabId+']');
				sTabText = jTab.find('span').html();
				jTab.find('span').html(sTabText+' <img style="vertical-align:bottom" src="../images/indicator.gif">');
			}
			$.post(sUrl, oParams, function(data) {
				var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='+data.att_id;
				var sIcon = GetAbsoluteUrlModulesRoot()+'itop-attachments/icons/pdf.png';
				if (jTab != null)
				{
					var re = /^([^(]+)\(([0-9]+)\)(.*)$/;
					var aParts = re.exec(sTabText);
					if (aParts == null)
					{
						// First attachment
						$('#attachments').html('<div class="attachment" id="display_attachment_'+data.att_id+'"><a data-preview="false" href="'+sDownloadLink+'"><img src="'+sIcon+'"><br/>'+sTitle+'.pdf<input id="attachment_'+data.att_id+'" type="hidden" name="attachments[]" value="'+data.att_id+'"/></a><br/><input type="button" class="btn_hidden" value="{$sDeleteBtn}" onClick="RemoveAttachment('+data.att_id+');"/></div>');
						jTab.find('span').html(sTabText +' (1)');
					}
					else
					{
						$('#attachments').append('<div class="attachment" id="display_attachment_'+data.att_id+'"><a data-preview="false" href="'+sDownloadLink+'"><img src="'+sIcon+'"><br/>'+sTitle+'.pdf<input id="attachment_'+data.att_id+'" type="hidden" name="attachments[]" value="'+data.att_id+'"/></a><br/><input type="button" class="btn_hidden" value="{$sDeleteBtn}" onClick="RemoveAttachment('+data.att_id+');"/></div>');
						var iPrevCount = parseInt(aParts[2], 10);
						jTab.find('span').html(aParts[1]+'('+(1 + iPrevCount)+')'+aParts[3]);						
					}
				}
			}, 'json');
			return false;
		},
		reload: function()
		{
			this.load_from_url(this.options.load_from_url);
		},
		_make_tooltips: function()
		{
			var me  = this;
			$( ".popupMenuTarget" ).tooltip({
				content: function() {
					var sDataId = $(this).attr('data-id');
					var sTooltipContent = '<div class="tooltip-close-button" data-id="'+sDataId+'" style="display:inline-block; float:right; cursor:pointer; padding-left:0.25em;">×</div>';
					sTooltipContent += me._get_tooltip_content(sDataId);
					return sTooltipContent;
				},
				items: '.popupMenuTarget',
				tooltipClass: 'tooltip-simple-graph',
				position: {
					using: function( position, feedback ) { 
						$(this).css( position );  
						$( "<div>" )
						.addClass( "arrow" )
						.addClass( feedback.vertical )
						.appendTo( this );
						}
				}
			})
			.off( "mouseover mouseout" )
			.on( "mouseover", function(event){
				event.stopImmediatePropagation();
				var jMe = $('text[data-id="'+$(this).attr('data-id')+'"]');
				jMe.data('openTimeoutId', setTimeout(function() {
					var sDataId = jMe.attr('data-id');
					if (jMe.tooltip())
					{
						jMe.data('openTimeoutId', 0);
						jMe.tooltip('open');
					}
				}, 1000));					
			})
			.on( "mouseout", function(event){
				event.stopImmediatePropagation();
				clearTimeout($('text[data-id="'+$(this).attr('data-id')+'"]').data('openTimeoutId'));					
			});
			/* Happens at every on_drag_end !!!
			.on( "click", function(){
				var sDataId = $(this).attr('data-id');
				if ($('.tooltip-close-button[data-id="'+sDataId+'"]').length == 0)
				{
					$(this).tooltip( 'open' );							 
				}
				else
				{
					$(this).tooltip( 'close' );							 						
				}           
				$( this ).unbind( "mouseleave" );
				return false;	
			 });
			*/
			$('body').on('click', '.tooltip-close-button', function() {
				var sDataId = $(this).attr('data-id');
				$('.popupMenuTarget[data-id="'+sDataId+'"]').tooltip('close');
			});
			this.element.on("click", ":not(.tooltip-simple-graph *,.tooltip-simple-graph)", function(){
				$('.popupMenuTarget').each(function (i) {
					clearTimeout($(this).data('openTimeoutId'));
					$(this).data('openTimeoutId', 0);
					$(this).tooltip("close"); 
				});
			});
		},
		_get_tooltip_content: function(sNodeId)
		{
			var oNode = this._find_node(sNodeId);
			if (oNode !== null)
			{
				return oNode.tooltip;
			}
			return '<p>Node Id:'+sNodeId+'</p>';
		},
		_close_all_tooltips: function()
		{
			this.element.find('.popupMenuTarget').each(function() {
				clearTimeout($(this).data('openTimeoutId'));
				$(this).data('openTimeoutId', 0);
				$(this).tooltip('close');
			});
		},
		_on_background_drag_start: function(x, y, event)
		{
			this.bDragging = true;
			this.xDrag = 0;
			this.yDrag = 0;
			//this._close_all_tooltips();
		},
		_on_background_move: function(dx, dy, x, y, event)
		{
			if (this.bDragging)
			{
				this.xDrag = dx;
				this.yDrag = dy;
				this.oPaper.setViewBox(this.xPan - this.xDrag, this.yPan - this.yDrag, this.element.width(),  this.element.height(), false);
			}
		},	
		_on_background_drag_end: function(event)
		{
			if (this.bDragging)
			{
				this.xPan -= this.xDrag;
				this.yPan -= this.yDrag;
				this.xDrag = 0;
				this.yDrag = 0;
				this.bDragging = false;
			}
		},
	});	
});
