simple_graph.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. // jQuery UI style "widget" for displaying a graph
  2. ////////////////////////////////////////////////////////////////////////////////
  3. //
  4. // graph
  5. //
  6. $(function()
  7. {
  8. // the widget definition, where "itop" is the namespace,
  9. // "dashboard" the widget name
  10. $.widget( "itop.simple_graph",
  11. {
  12. // default options
  13. options:
  14. {
  15. align: 'center',
  16. 'vertical-align': 'middle',
  17. source_url: null,
  18. export_as_pdf: null,
  19. page_format: { label: 'Page Format:', values: { A3: 'A3', A4: 'A4', Letter: 'Letter' }, 'default': 'A4'},
  20. page_orientation: { label: 'Page Orientation:', values: { P: 'Portait', L: 'Landscape' }, 'default': 'L' },
  21. labels: { export_pdf_title: 'PDF Export Options', cancel: 'Cancel', 'export': 'Export', title: 'Document Title', include_list: 'Include the list of objects', comments: 'Comments' },
  22. export_as_document: null,
  23. drill_down: null,
  24. excluded: []
  25. },
  26. // the constructor
  27. _create: function()
  28. {
  29. var me = this;
  30. this.aNodes = [];
  31. this.aEdges = [];
  32. this.fZoom = 1.0;
  33. this.xOffset = 0;
  34. this.yOffset = 0;
  35. this.iTextHeight = 12;
  36. this.oPaper = Raphael(this.element.get(0), this.element.width(), this.element.height());
  37. this.element
  38. .addClass('panel-resized')
  39. .addClass('itop-simple-graph')
  40. .addClass('graph');
  41. this._create_toolkit_menu();
  42. this._build_context_menus();
  43. $(window).bind('resized', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } );
  44. if (this.options.source_url != null)
  45. {
  46. this.load_from_url(this.options.source_url);
  47. }
  48. },
  49. // called when created, and later when changing options
  50. _refresh: function()
  51. {
  52. this.draw();
  53. },
  54. // events bound via _bind are removed automatically
  55. // revert other modifications here
  56. _destroy: function()
  57. {
  58. var sId = this.element.attr('id');
  59. this.element
  60. .removeClass('itop-simple-graph')
  61. .removeClass('graph');
  62. $('#tk_graph'+sId).remove();
  63. $('#graph_'+sId+'_export_as_pdf').remove();
  64. },
  65. // _setOptions is called with a hash of all options that are changing
  66. _setOptions: function()
  67. {
  68. this._superApply(arguments);
  69. },
  70. // _setOption is called for each individual option that is changing
  71. _setOption: function( key, value )
  72. {
  73. this._superApply(arguments);
  74. },
  75. draw: function()
  76. {
  77. this._updateBBox();
  78. this.auto_scale();
  79. this.oPaper.clear();
  80. for(var k in this.aNodes)
  81. {
  82. this.aNodes[k].aElements = [];
  83. this._draw_node(this.aNodes[k]);
  84. }
  85. for(var k in this.aEdges)
  86. {
  87. this.aEdges[k].aElements = [];
  88. this._draw_edge(this.aEdges[k]);
  89. }
  90. },
  91. _draw_node: function(oNode)
  92. {
  93. var iWidth = oNode.width;
  94. var iHeight = 32;
  95. var iFontSize = 10;
  96. var xPos = Math.round(oNode.x * this.fZoom + this.xOffset);
  97. var yPos = Math.round(oNode.y * this.fZoom + this.yOffset);
  98. oNode.tx = 0;
  99. oNode.ty = 0;
  100. switch(oNode.shape)
  101. {
  102. case 'disc':
  103. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
  104. var oText = this.oPaper.text(xPos, yPos, oNode.label);
  105. oNode.text_attr['font-size'] = iFontSize * this.fZoom;
  106. oText.attr(oNode.text_attr);
  107. //oText.transform('s'+this.fZoom);
  108. oNode.aElements.push(oText);
  109. break;
  110. case 'group':
  111. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr({fill: '#fff', 'stroke-width':0}));
  112. oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
  113. var xIcon = xPos - 18 * this.fZoom;
  114. var yIcon = yPos - 18 * this.fZoom;
  115. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
  116. oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*this.fZoom, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
  117. 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));
  118. var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
  119. oNode.text_attr['font-size'] = iFontSize * this.fZoom;
  120. oText.attr(oNode.text_attr);
  121. //oText.transform('s'+this.fZoom);
  122. var oBB = oText.getBBox();
  123. var dy = iHeight/2*this.fZoom + oBB.height/2;
  124. oText.remove();
  125. oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label);
  126. oText.attr(oNode.text_attr);
  127. //oText.transform('s'+this.fZoom);
  128. oNode.aElements.push(oText);
  129. 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}));
  130. oText.toFront();
  131. break;
  132. case 'icon':
  133. if(Raphael.svg)
  134. {
  135. // the colorShift plugin works only in SVG
  136. 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).colorShift('#fff', 1));
  137. }
  138. 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));
  139. var oText = this.oPaper.text( xPos, yPos, oNode.label);
  140. oNode.text_attr['font-size'] = iFontSize * this.fZoom;
  141. oText.attr(oNode.text_attr);
  142. //oText.transform('S'+this.fZoom);
  143. var oBB = oText.getBBox();
  144. var dy = iHeight/2*this.fZoom + oBB.height/2;
  145. oText.remove();
  146. oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
  147. oText.attr(oNode.text_attr);
  148. //oText.transform('S'+this.fZoom);
  149. oNode.aElements.push(oText);
  150. 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());
  151. break;
  152. }
  153. if (oNode.source)
  154. {
  155. oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#c33', 'stroke-width': 3*this.fZoom }).toBack());
  156. }
  157. if (oNode.sink)
  158. {
  159. oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#33c', 'stroke-width': 3*this.fZoom }).toBack());
  160. }
  161. var me = this;
  162. for(k in oNode.aElements)
  163. {
  164. var sNodeId = oNode.id;
  165. $(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget');
  166. oNode.aElements[k].drag(function(dx, dy, x, y, event) { 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); });
  167. }
  168. },
  169. _move: function(sNodeId, dx, dy, x, y, event)
  170. {
  171. var origDx = dx / this.fZoom;
  172. var origDy = dy / this.fZoom;
  173. var oNode = this._find_node(sNodeId);
  174. oNode.x = oNode.xOrig + origDx;
  175. oNode.y = oNode.yOrig + origDy;
  176. for(k in oNode.aElements)
  177. {
  178. oNode.aElements[k].transform('t'+(oNode.tx + dx)+', '+(oNode.ty + dy));
  179. for(j in this.aEdges)
  180. {
  181. var oEdge = this.aEdges[j];
  182. if ((oEdge.source_node_id == sNodeId) || (oEdge.sink_node_id == sNodeId))
  183. {
  184. var sPath = this._get_edge_path(oEdge);
  185. oEdge.aElements[0].attr({path: sPath});
  186. }
  187. }
  188. }
  189. },
  190. _drag_start: function(sNodeId, x, y, event)
  191. {
  192. var oNode = this._find_node(sNodeId);
  193. oNode.xOrig = oNode.x;
  194. oNode.yOrig = oNode.y;
  195. },
  196. _drag_end: function(sNodeId, event)
  197. {
  198. var oNode = this._find_node(sNodeId);
  199. oNode.tx += (oNode.x - oNode.xOrig) * this.fZoom;
  200. oNode.ty += (oNode.y - oNode.yOrig) * this.fZoom;
  201. oNode.xOrig = oNode.x;
  202. oNode.yOrig = oNode.y;
  203. this._updateBBox();
  204. },
  205. _updateBBox: function()
  206. {
  207. this.options.xmin = 9999;
  208. this.options.xmax = -9999;
  209. this.options.ymin = 9999;
  210. this.options.ymax = -9999;
  211. for(var k in this.aNodes)
  212. {
  213. this.options.xmin = Math.min(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmin);
  214. this.options.xmax = Math.max(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmax);
  215. this.options.ymin = Math.min(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymin);
  216. this.options.ymax = Math.max(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymax);
  217. }
  218. },
  219. _get_edge_path: function(oEdge)
  220. {
  221. var oStart = this._find_node(oEdge.source_node_id);
  222. var oEnd = this._find_node(oEdge.sink_node_id);
  223. var iArrowSize = 5;
  224. if ((oStart == null) || (oEnd == null)) return '';
  225. var xStart = Math.round(oStart.x * this.fZoom + this.xOffset);
  226. var yStart = Math.round(oStart.y * this.fZoom + this.yOffset);
  227. var xEnd = Math.round(oEnd.x * this.fZoom + this.xOffset);
  228. var yEnd = Math.round(oEnd.y * this.fZoom + this.yOffset);
  229. var sPath = Raphael.format('M{0},{1}L{2},{3}', xStart, yStart, xEnd, yEnd);
  230. var vx = (xEnd - xStart);
  231. var vy = (yEnd - yStart);
  232. var l = Math.sqrt(vx*vx+vy*vy);
  233. vx = vx / l;
  234. vy = vy / l;
  235. var ux = -vy;
  236. var uy = vx;
  237. var lPos = Math.max(l/2, l - 40*this.fZoom);
  238. var xArrow = xStart + vx * lPos;
  239. var yArrow = yStart + vy * lPos;
  240. sPath += Raphael.format('M{0},{1}l{2},{3}M{4},{5}l{6},{7}', xArrow, yArrow, this.fZoom * iArrowSize *(-vx + ux), this.fZoom * iArrowSize *(-vy + uy), xArrow, yArrow, this.fZoom * iArrowSize *(-vx - ux), this.fZoom * iArrowSize *(-vy - uy));
  241. return sPath;
  242. },
  243. _draw_edge: function(oEdge)
  244. {
  245. var fStrokeSize = Math.max(1, 2 * this.fZoom);
  246. var sPath = this._get_edge_path(oEdge);
  247. var oAttr = $.extend(oEdge.attr);
  248. oAttr['stroke-linecap'] = 'round';
  249. oAttr['stroke-width'] = fStrokeSize;
  250. oEdge.aElements.push(this.oPaper.path(sPath).attr(oAttr).toBack());
  251. },
  252. _find_node: function(sId)
  253. {
  254. for(var k in this.aNodes)
  255. {
  256. if (this.aNodes[k].id == sId) return this.aNodes[k];
  257. }
  258. return null;
  259. },
  260. auto_scale: function()
  261. {
  262. var fMaxZoom = 1.5;
  263. var maxHeight = this.element.parent().height();
  264. // Compute the available height
  265. var element = this.element;
  266. this.element.parent().children().each(function() {
  267. if($(this).is(':visible') && !$(this).hasClass('graph') && ($(this).attr('id') != element.attr('id')))
  268. {
  269. maxHeight = maxHeight - $(this).height();
  270. }
  271. });
  272. this.element.height(maxHeight - 20);
  273. iMargin = 10;
  274. xmin = this.options.xmin - iMargin;
  275. xmax = this.options.xmax + iMargin;
  276. ymin = this.options.ymin - iMargin;
  277. ymax = this.options.ymax + iMargin;
  278. var xScale = this.element.width() / (xmax - xmin);
  279. var yScale = this.element.height() / (ymax - ymin + this.iTextHeight);
  280. this.fZoom = Math.min(xScale, yScale, fMaxZoom);
  281. switch(this.options.align)
  282. {
  283. case 'left':
  284. this.xOffset = -xmin * this.fZoom;
  285. break;
  286. case 'right':
  287. this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom);
  288. break;
  289. case 'center':
  290. this.xOffset = -xmin * this.fZoom + (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
  291. break;
  292. }
  293. switch(this.options['vertical-align'])
  294. {
  295. case 'top':
  296. this.yOffset = -ymin * this.fZoom;
  297. break;
  298. case 'bottom':
  299. this.yOffset = this.element.height() - (ymax + this.iTextHeight) * this.fZoom;
  300. break;
  301. case 'middle':
  302. this.yOffset = -ymin * this.fZoom + (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
  303. break;
  304. }
  305. },
  306. add_node: function(oNode)
  307. {
  308. oNode.aElements = [];
  309. oNode.tx = 0;
  310. oNode.ty = 0;
  311. this.aNodes.push(oNode);
  312. },
  313. add_edge: function(oEdge)
  314. {
  315. oEdge.aElements = [];
  316. this.aEdges.push(oEdge);
  317. },
  318. show_group: function(sGroupId)
  319. {
  320. // Activate the 3rd tab
  321. this.element.closest('.ui-tabs').tabs("option", "active", 2);
  322. // Scroll into view the group
  323. if ($('#'+sGroupId).length > 0)
  324. {
  325. $('#'+sGroupId)[0].scrollIntoView();
  326. }
  327. },
  328. _create_toolkit_menu: function()
  329. {
  330. var sPopupMenuId = 'tk_graph'+this.element.attr('id');
  331. var sHtml = '<div class="itop_popup toolkit_menu graph" style="font-size: 12px;" id="'+sPopupMenuId+'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
  332. if (this.options.export_as_pdf != null)
  333. {
  334. sHtml += '<li><a href="#" id="'+sPopupMenuId+'_pdf">'+this.options.export_as_pdf.label+'</a></li>';
  335. }
  336. if (this.options.export_as_document != null)
  337. {
  338. sHtml += '<li><a href="#" id="'+sPopupMenuId+'_document">'+this.options.export_as_document.label+'</a></li>';
  339. }
  340. //sHtml += '<li><a href="#" id="'+sPopupMenuId+'_reload">Refresh</a></li>';
  341. sHtml += '</ul></li></ul></div>';
  342. this.element.before(sHtml);
  343. $('#'+sPopupMenuId).popupmenu();
  344. var me = this;
  345. $('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
  346. $('#'+sPopupMenuId+'_document').click(function() { me.export_as_document(); });
  347. $('#'+sPopupMenuId+'_reload').click(function() { me.reload(); });
  348. },
  349. _build_context_menus: function()
  350. {
  351. var sId = this.element.attr('id');
  352. var me = this;
  353. $.contextMenu({
  354. selector: '#'+sId+' .popupMenuTarget',
  355. build: function(trigger, e) {
  356. // this callback is executed every time the menu is to be shown
  357. // its results are destroyed every time the menu is hidden
  358. // e is the original contextmenu event, containing e.pageX and e.pageY (amongst other data)
  359. var sType = trigger.attr('data-type');
  360. var sNodeId = trigger.attr('data-id');
  361. var oNode = me._find_node(sNodeId);
  362. /*
  363. var sObjName = trigger.attr('data-class');
  364. var sIndex = trigger.attr('data-index');
  365. var originalEvent = e;
  366. var bHasItems = false;
  367. */
  368. var oResult = {callback: null, items: {}};
  369. switch(sType)
  370. {
  371. case 'group':
  372. var sGroupIndex = oNode.group_index;
  373. oResult = {
  374. callback: function(key, options) {
  375. var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
  376. me.show_group('relation_group_'+sGroupIndex);
  377. },
  378. items: { 'show': {name: me.options.drill_down.label } }
  379. };
  380. break;
  381. case 'icon':
  382. var sObjClass = oNode.obj_class;
  383. var sObjKey = oNode.obj_key;
  384. oResult = {
  385. callback: function(key, options) {
  386. var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
  387. var sURL = me.options.drill_down.url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
  388. window.location.href = sURL;
  389. },
  390. items: { 'details': {name: me.options.drill_down.label } }
  391. };
  392. break;
  393. default:
  394. oResult = false; // No context menu
  395. }
  396. return oResult;
  397. }
  398. });
  399. },
  400. export_as_pdf: function()
  401. {
  402. var oPositions = {};
  403. for(k in this.aNodes)
  404. {
  405. oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
  406. }
  407. 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">';
  408. sHtmlForm += '<input type="hidden" name="positions" value="">';
  409. for(k in this.options.excluded)
  410. {
  411. sHtmlForm += '<input type="hidden" name="excluded[]" value="'+this.options.excluded[k]+'">';
  412. }
  413. sHtmlForm += '<table>';
  414. sHtmlForm += '<tr><td>'+this.options.page_format.label+'</td><td><select name="p">';
  415. for(k in this.options.page_format.values)
  416. {
  417. var sSelected = (k == this.options.page_format['default']) ? ' selected' : '';
  418. sHtmlForm += '<option value="'+k+'"'+sSelected+'>'+this.options.page_format.values[k]+'</option>';
  419. }
  420. sHtmlForm += '</select></td></tr>';
  421. sHtmlForm += '<tr><td>'+this.options.page_orientation.label+'</td><td><select name="o">';
  422. for(k in this.options.page_orientation.values)
  423. {
  424. var sSelected = (k == this.options.page_orientation['default']) ? ' selected' : '';
  425. sHtmlForm += '<option value="'+k+'"'+sSelected+'>'+this.options.page_orientation.values[k]+'</option>';
  426. }
  427. sHtmlForm += '</select></td></tr>';
  428. sHtmlForm += '<tr><td>'+this.options.labels.title+'</td><td><input name="title" style="width: 20em;"/></td></tr>';
  429. sHtmlForm += '<tr><td>'+this.options.labels.comments+'</td><td><textarea style="width: 20em; height:5em;" name="comments"/></textarea></td></tr>';
  430. 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>';
  431. sHtmlForm += '<table>';
  432. sHtmlForm += '</form></div>';
  433. $('body').append(sHtmlForm);
  434. $('#graph_'+this.element.attr('id')+'_export_as_pdf input[name="positions"]').val(JSON.stringify(oPositions));
  435. var me = this;
  436. $('#PDFExportDlg'+this.element.attr('id')).dialog({
  437. width: 'auto',
  438. modal: true,
  439. title: this.options.labels.export_pdf_title,
  440. close: function() { $(this).remove(); },
  441. buttons: [
  442. {text: this.options.labels['cancel'], click: function() { $(this).dialog('close');} },
  443. {text: this.options.labels['export'], click: function() { $('#graph_'+me.element.attr('id')+'_export_as_pdf').submit(); $(this).dialog('close');} },
  444. ]
  445. });
  446. //$('#graph_'+this.element.attr('id')+'_export_as_pdf').submit();
  447. },
  448. _on_resize: function()
  449. {
  450. this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
  451. this.auto_scale();
  452. this.oPaper.setSize(this.element.width(), this.element.height());
  453. this.draw();
  454. },
  455. load: function(oData)
  456. {
  457. this.aNodes = [];
  458. this.aEdges = [];
  459. for(k in oData.nodes)
  460. {
  461. this.add_node(oData.nodes[k]);
  462. }
  463. for(k in oData.edges)
  464. {
  465. this.add_edge(oData.edges[k]);
  466. }
  467. this._updateBBox();
  468. this.auto_scale();
  469. this.oPaper.setSize(this.element.width(), this.element.height());
  470. this.draw();
  471. },
  472. load_from_url: function(sUrl)
  473. {
  474. this.options.load_from_url = sUrl;
  475. var me = this;
  476. this.element.closest('.ui-tabs').tabs({ heightStyle: "fill" });
  477. this.oPaper.rect(0, 0, this.element.width(), this.element.height()).attr({fill: '#000', opacity: 0.4, 'stroke-width': 0});
  478. $.post(sUrl, {excluded: this.options.excluded }, function(data) {
  479. me.load(data);
  480. }, 'json');
  481. },
  482. export_as_document: function()
  483. {
  484. alert('Export as document: not yet implemented');
  485. },
  486. reload: function()
  487. {
  488. this.load_from_url(this.options.load_from_url);
  489. }
  490. });
  491. });