// 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,
					transaction_id: 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: ''
				},
			css_classes:
				{
					has_focus: 'ibo-has-focus'
				},
			// 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), this.element.width(), screen.availHeight * 2 / 3);

				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('.ibo-tab-container');
					jTabs.on("tabsactivate", function (event, ui) {
						me._on_tabs_activate(ui);
					});
				}
				$(window).on('resized', function () {
					var that = me;
					window.setTimeout(function () {
						that._on_resize();
					}, 50);
				});
				$('#dh_flash').on('toggle_complete', function () {
					var that = me;
					window.setTimeout(function () {
						that._on_resize();
					}, 50);
				});
				this.element.on('mousewheel', function (event, delta, deltaX, deltaY) {
					return me._on_mousewheel(event, delta, deltaX, deltaY);
				});
				$(document).on('click', function (e) {
					if ($(e.target).closest(me.element).length === 0) {
						me.element.removeClass(me.css_classes.has_focus);
					} else {
						me.element.addClass(me.css_classes.has_focus);
					}
				});
				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;
			},
			auto_scale: function()
			{
				var fMaxZoom = 1.5;

				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('[data-role="ibo-tab-container"]').tab_container("GetTabsWidget").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 += '<div class="ibo-simple-graph--grouping-threshold--container"><label for="'+sId+'_grouping_threshold">'+this.options.labels.grouping_threshold+'</label><input type="number" name="g" value="'+this.options.grouping_threshold+'" id="'+sId+'_grouping_threshold" size="2" class="ibo-input"></div>';
				if (this.options.additional_contexts.length > 0)
				{
					sHtml += '<div class="ibo-simple-graph--additional-context--container"><label for="'+sId+'_contexts">'+this.options.labels.additional_context_info+'</label><div><select id="'+sId+'_contexts" name="contexts" class="multiselect ibo-input' +
						' ibo-input-select" multiple size="1">';
					for(var k in this.options.additional_contexts)
					{
						var sSelected = (this.options.additional_contexts[k]['default']) ? 'selected' : '';
						sHtml += '<option value="'+k+'" '+sSelected+'>'+this.options.additional_contexts[k].label+'</option>';
					}
					sHtml += '</select></div></div>'
				}
				sHtml += '<button type="button" id="'+sId+'_refresh_btn" class="ibo-button ibo-is-neutral ibo-is-regular">'+this.options.labels.refresh+'</button>';
				sHtml += '<div class="graph_separator"></div>';
				sHtml += '<div class="graph_zoom"><label for"'+sId+'_zoom">'+this.options.labels.zoom+'</label>';
				sHtml += '<div id="'+sId+'_zoom_minus" class="graph_zoom_minus ui-icon ui-icon-circle-minus"><i class="fas fa-search-minus"></i></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"><i class="fas fa-search-plus"></i></div>';
				sHtml += '<div class="itop_popup toolkit_menu graph" 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 += '</div>';
				sHtml += '</div>';


				this.element.before(sHtml);
				$('#'+sPopupMenuId+'>ul').popupmenu();


				var me = this;
				$('#'+sPopupMenuId+'_pdf').on('click', function() { me.export_as_pdf(); });
				$('#'+sPopupMenuId+'_attachment').on('click', function() { me.export_as_attachment(); });
				$('#'+sId+'_zoom').slider({ min: 0, max: 5, value: 1, step: 0.25, change: function() { me._on_zoom_change( $(this).slider('value')); } });
				$('#'+sId+'_zoom_plus').on('click', function() { $('#'+sId+'_zoom').slider('value', 0.25 + $('#'+sId+'_zoom').slider('value')); return false; });
				$('#'+sId+'_zoom_minus').on('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().on('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+'">';
				sHtmlForm += '<input type="hidden" name="transaction_id" value="'+this.options.transaction_id+'">';
				$('#'+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').on('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').trigger('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)
			{
				if(this.element.hasClass(this.css_classes.has_focus))
				{
					var fStep = 0.25*delta;
					var sId = this.element.attr('id');
					$('#'+sId+'_zoom').slider('value', fStep + $('#'+sId+'_zoom').slider('value'));
				}
			},
			_on_resize: function()
			{
				this.auto_scale();
				this._close_all_tooltips();
				this.draw();
			},
			_on_tabs_activate: function(ui)
			{
				if (ui.newPanel[0] === $('#'+this.sTabId)[0])
				{
					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)
				{
					if (this.options.excluded_classes.length > 0) {
						var newList = {};
						$.each(oData.lists, function (index, listId) {
							if (me.options.excluded_classes.indexOf(index) < 0) {
								newList[index] = listId;
							}
						});
						me.refresh_lists(newList);
						if ($('#alert_filtered_list').length > 0) {
							$('#alert_filtered_list').removeClass('ibo-is-hidden');
						}
					} else {
						me.refresh_lists(oData.lists);
						if ($('#alert_filtered_list').length > 0) {
							$('#alert_filtered_list').addClass('ibo-is-hidden');
						}
					}
				}
				if (me.element.is(':visible')) {
					me._updateBBox();
					me.auto_scale();
					me._reset_pan_and_zoom();
					me.draw();
				} else {
					me.bRedrawNeeded = true;
				}
			},
			refresh_groups: function(aGroups)
			{
				if(this.element.parents('.ibo-tab-container').attr('data-status') === 'loaded'){
					if ($('#impacted_groups').length > 0)
					{
						// The "Groups" tab is present, refresh it
						if (aGroups.length == 0)
						{
							this.element.closest('[data-role="ibo-tab-container"]').tab_container("GetTabsWidget").disable(2);
							$('#impacted_groups').html('');
						}
						else
						{
							this.element.closest('[data-role="ibo-tab-container"]').tab_container("GetTabsWidget").enable(2);
							$('#impacted_groups').block({message:this.options.labels.loading});
							var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
							$.post(sUrl, { operation: 'relation_groups', groups: aGroups }, function(data) {
								$('#impacted_groups').unblock();
								$('#impacted_groups').html(data);
							});
						}
					}
				}
				else{
					setTimeout(this.refresh_groups(aGroups), 800);
				}
			},
			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').block({message:this.options.labels.loading});
						var sUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
						$.post(sUrl, { operation: 'relation_lists', lists: aLists }, function(data) {
							$('#impacted_objects_lists').unblock();
							$('#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; });

				this._close_all_tooltips();
				this.element.block({message: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);
					me.element.unblock();
					$('#'+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/icons8-pdf.svg';
					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;
				let aTooltipGroups = [];
				$( ".popupMenuTarget" ).each(function(){
					var sDataId = $(this).attr('data-id');
					var sTooltipContent = me._get_tooltip_content(sDataId);
					$(this).attr('data-tooltip-content', sTooltipContent)
						.attr('data-tooltip-html-enabled', 'true')
						.attr('data-tooltip-interaction-enabled', 'true')
						.attr('data-tooltip-append-to', 'body')
						.attr('data-tooltip-hide-delay', '1500');
					CombodoTooltip.InitTooltipFromMarkup($(this));
					if(aTooltipGroups.indexOf(sDataId) < 0) {
						aTooltipGroups.push(sDataId);
					}
				});
				for(let sTooltipGroupKey in aTooltipGroups) {
					CombodoTooltip.InitSingletonFromSelector('.itop-simple-graph [data-id="' + aTooltipGroups[sTooltipGroupKey] + '"]');
				}
			},
			_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()
			{
				//obsolete
			},
			_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;
				}
			},
		});
});
