diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php
index f31e96e20..0d63733b0 100644
--- a/application/itopwebpage.class.inc.php
+++ b/application/itopwebpage.class.inc.php
@@ -171,6 +171,7 @@ EOF;
var innerWidth = $(this).innerWidth() - 10;
$(this).find('.item').width(innerWidth);
});
+ $('.panel-resized').trigger('resized');
}
}
@@ -217,7 +218,7 @@ EOF;
},
beforeLoad: function( event, ui ) {
if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) {
- event.preventDefault();
+ event.defaultPrevented = true;
return;
}
ui.panel.html('
');
@@ -297,6 +298,21 @@ EOF
$.bbq.pushState( state );
});
+ // refresh the hash when the tab is changed (from a JS script)
+ $('body').on( 'tabsactivate', '.ui-tabs', function(event, ui) {
+ var state = {};
+
+ // Get the id of this tab widget.
+ var id = $(ui.newTab).closest( 'div[id^=tabbedContent]' ).attr( 'id' );
+
+ // Get the index of this tab.
+ var idx = $(ui.newTab).prevAll().length;
+
+ // Set the state!
+ state[ id ] = idx;
+ $.bbq.pushState( state );
+ });
+
// Bind an event to window.onhashchange that, when the history state changes,
// iterates over all tab widgets, changing the current tab as necessary.
$(window).bind( 'hashchange', function(e)
diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php
index 06497954f..778bfa90b 100644
--- a/core/displayablegraph.class.inc.php
+++ b/core/displayablegraph.class.inc.php
@@ -584,6 +584,13 @@ class DisplayableGraph extends SimpleGraph
}
}
+ /**
+ * Build a DisplayableGraph from a RelationGraph
+ * @param RelationGraph $oGraph
+ * @param number $iGroupingThreshold
+ * @param string $bDirectionDown
+ * @return DisplayableGraph
+ */
public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
{
$oNewGraph = new DisplayableGraph();
@@ -697,6 +704,11 @@ class DisplayableGraph extends SimpleGraph
return $oNewGraph;
}
+ /**
+ * Initializes the positions by rendering using Graphviz in xdot format
+ * and parsing the output.
+ * @throws Exception
+ */
public function InitFromGraphviz()
{
$sDot = $this->DumpAsXDot();
@@ -780,22 +792,45 @@ class DisplayableGraph extends SimpleGraph
}
}
+ /**
+ * Renders as a suite of Javascript instructions to display the graph using the simple_graph widget
+ * @param WebPage $oP
+ * @param string $sId
+ * @param string $sExportAsPdfURL
+ * @param string $sExportAsDocumentURL
+ * @param string $sDrillDownURL
+ */
function RenderAsRaphael(WebPage $oP, $sId = null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL)
{
if ($sId == null)
{
$sId = 'graph';
}
- $aBB = $this->GetBoundingBox();
$oP->add('');
$aParams = array(
- 'xmin' => $aBB['xmin'],
- 'xmax' => $aBB['xmax'],
- 'ymin' => $aBB['ymin'],
- 'ymax' => $aBB['ymax'],
- 'export_as_pdf_url' => $sExportAsPdfURL,
- 'export_as_document_url' => $sExportAsDocumentURL,
- 'drill_down_url' => $sDrillDownURL,
+ 'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
+ 'export_as_document' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsDocument')),
+ 'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
+ 'labels' => array(
+ 'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
+ 'export' => Dict::S('UI:Relation:PDFExportOptions'),
+ 'cancel' => Dict::S('UI:Button:Cancel'),
+ ),
+ 'page_format' => array(
+ 'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
+ 'values' => array(
+ 'A3' => Dict::S('UI:PageFormat_A3'),
+ 'A4' => Dict::S('UI:PageFormat_A4'),
+ 'Letter' => Dict::S('UI:PageFormat_Letter'),
+ ),
+ ),
+ 'page_orientation' => array(
+ 'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
+ 'values' => array(
+ 'P' => Dict::S('UI:PageOrientation_Portrait'),
+ 'L' => Dict::S('UI:PageOrientation_Landscape'),
+ ),
+ ),
);
$oP->add_ready_script("var oGraph = $('#$sId').simple_graph(".json_encode($aParams).");");
@@ -822,6 +857,47 @@ class DisplayableGraph extends SimpleGraph
$oP->add_ready_script("oGraph.simple_graph('draw');");
}
+ /**
+ * Renders as JSON string suitable for loading into the simple_graph widget
+ */
+ function GetAsJSON()
+ {
+ $aData = array('nodes' => array(), 'edges' => array());
+ $iGroupIdx = 0;
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ if ($oNode instanceof DisplayableGroupNode)
+ {
+ $aGroups[] = $oNode->GetObjects();
+ $oNode->SetProperty('group_index', $iGroupIdx);
+ $iGroupIdx++;
+ }
+ $aData['nodes'][] = $oNode->GetForRaphael();
+ }
+
+ $oIterator = new RelationTypeIterator($this, 'Edge');
+ foreach($oIterator as $sId => $oEdge)
+ {
+ $aEdge = array();
+ $aEdge['id'] = $oEdge->GetId();
+ $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
+ $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
+ $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
+ $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
+ $aData['edges'][] = $aEdge;
+ }
+
+ return json_encode($aData);
+ }
+
+ /**
+ * Renders the graph as a PDF file
+ * @param WebPage $oP The page for the ouput of the PDF
+ * @param string $sTitle The title of the PDF
+ * @param string $sPageFormat The page format: A4, A3, Letter...
+ * @param string $sPageOrientation The orientation of the page (L = Landscape, P = Portrait)
+ */
function RenderAsPDF(WebPage $oP, $sTitle = 'Untitled', $sPageFormat = 'A4', $sPageOrientation = 'P')
{
require_once(APPROOT.'lib/tcpdf/tcpdf.php');
diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php
index 66b859ea3..6b7f22d5d 100644
--- a/dictionaries/dictionary.itop.ui.php
+++ b/dictionaries/dictionary.itop.ui.php
@@ -965,9 +965,22 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:DisplayThisMessageAtStartup' => 'Display this message at startup',
'UI:RelationshipGraph' => 'Graphical view',
'UI:RelationshipList' => 'List',
+ 'UI:RelationGroups' => 'Groups',
'UI:OperationCancelled' => 'Operation Cancelled',
'UI:ElementsDisplayed' => 'Filtering',
-
+ 'UI:RelationGroupNumber_N' => 'Group #%1$d',
+ 'UI:Relation:ExportAsPDF' => 'Export as PDF...',
+ 'UI:Relation:ExportAsDocument' => 'Export as Document...',
+ 'UI:Relation:DrillDown' => 'Details...',
+ 'UI:Relation:PDFExportOptions' => 'PDF Export Options',
+ 'UI:Button:Export' => 'Export',
+ 'UI:Relation:PDFExportPageFormat' => 'Page format',
+ 'UI:PageFormat_A3' => 'A3',
+ 'UI:PageFormat_A4' => 'A4',
+ 'UI:PageFormat_Letter' => 'Letter',
+ 'UI:Relation:PDFExportPageOrientation' => 'Page orientation',
+ 'UI:PageOrientation_Portrait' => 'Portrait',
+ 'UI:PageOrientation_Landscape' => 'Landscape',
'Portal:Title' => 'iTop user portal',
'Portal:NoRequestMgmt' => 'Dear %1$s, you have been redirected to this page because your account is configured with the profile \'Portal user\'. Unfortunately, iTop has not been installed with the feature \'Request Management\'. Please contact your administrator.',
'Portal:Refresh' => 'Refresh',
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index 8808c9d86..de6b5930e 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -809,7 +809,21 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:DisplayThisMessageAtStartup' => 'Afficher ce message au démarrage',
'UI:RelationshipGraph' => 'Vue graphique',
'UI:RelationshipList' => 'Liste',
+ 'UI:RelationGroups' => 'Groupes',
'UI:ElementsDisplayed' => 'Filtrage',
+ 'UI:RelationGroupNumber_N' => 'Groupe n°%1$d',
+ 'UI:Relation:ExportAsPDF' => 'Exportation en PDF...',
+ 'UI:Relation:ExportAsDocument' => 'Exportation comme Document...',
+ 'UI:Relation:DrillDown' => 'Détails...',
+ 'UI:Relation:PDFExportOptions' => 'Options de l\'Exportation PDF',
+ 'UI:Button:Export' => 'Exporter',
+ 'UI:Relation:PDFExportPageFormat' => 'Format de page',
+ 'UI:PageFormat_A3' => 'A3',
+ 'UI:PageFormat_A4' => 'A4',
+ 'UI:PageFormat_Letter' => 'Letter',
+ 'UI:Relation:PDFExportPageOrientation' => 'Orientation de la page',
+ 'UI:PageOrientation_Portrait' => 'Portrait',
+ 'UI:PageOrientation_Landscape' => 'Paysage',
'UI:OperationCancelled' => 'Opération Annulée',
'Portal:Title' => 'Portail utilisateur iTop',
'Portal:NoRequestMgmt' => 'Chèr(e) %1$s, vous avez été redirigé(e) vers cette page car votre compte utilisateur est configuré avec le profil \'Utilisateur du Portail\'. Malheureusement, iTop n\'a pas été installé avec le module de \'Gestion des Demandes\'. Merci de contacter votre administrateur iTop.',
diff --git a/js/simple_graph.js b/js/simple_graph.js
index fbe743216..00e9cf972 100644
--- a/js/simple_graph.js
+++ b/js/simple_graph.js
@@ -13,15 +13,16 @@ $(function()
// default options
options:
{
- xmin: 0,
- xmax: 0,
- ymin: 0,
- ymax: 0,
align: 'center',
'vertical-align': 'middle',
- export_as_pdf_url: '',
- export_as_document_url: '',
- drill_down_url: '',
+ source_url: null,
+ export_as_pdf: null,
+ page_format: { label: 'Page Format:', values: { A3: 'A3', A4: 'A4', Letter: 'Letter' }, 'default': 'A4'},
+ page_orientation: { label: 'Page Orientation:', values: { P: 'Portait', L: 'Landscape' }, 'default': 'L' },
+ labels: { export_pdf_title: 'PDF Export Options', cancel: 'Cancel', 'export': 'Export' },
+ export_as_document: null,
+ drill_down: null,
+ excluded: []
},
// the constructor
@@ -35,17 +36,21 @@ $(function()
this.yOffset = 0;
this.iTextHeight = 12;
- this.auto_scale();
this.oPaper = Raphael(this.element.get(0), this.element.width(), this.element.height());
this.element
+ .addClass('panel-resized')
.addClass('itop-simple-graph')
.addClass('graph');
this._create_toolkit_menu();
this._build_context_menus();
+ $(window).bind('resized', function() { var that = me; window.setTimeout(function() { that._on_resize(); }, 50); } );
+ if (this.options.source_url != null)
+ {
+ this.load_from_url(this.options.source_url);
+ }
},
-
// called when created, and later when changing options
_refresh: function()
{
@@ -76,13 +81,17 @@ $(function()
},
draw: function()
{
+ this._updateBBox();
+ this.auto_scale();
this.oPaper.clear();
for(var k in this.aNodes)
{
+ this.aNodes[k].aElements = [];
this._draw_node(this.aNodes[k]);
}
for(var k in this.aEdges)
{
+ this.aEdges[k].aElements = [];
this._draw_edge(this.aEdges[k]);
}
},
@@ -90,6 +99,7 @@ $(function()
{
var iWidth = oNode.width;
var iHeight = 32;
+ var iFontSize = 10;
var xPos = Math.round(oNode.x * this.fZoom + this.xOffset);
var yPos = Math.round(oNode.y * this.fZoom + this.yOffset);
oNode.tx = 0;
@@ -99,8 +109,9 @@ $(function()
case 'disc':
oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
var oText = this.oPaper.text(xPos, yPos, oNode.label);
+ oNode.text_attr['font-size'] = iFontSize * this.fZoom;
oText.attr(oNode.text_attr);
- oText.transform('s'+this.fZoom);
+ //oText.transform('s'+this.fZoom);
oNode.aElements.push(oText);
break;
@@ -113,14 +124,15 @@ $(function()
oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*this.fZoom, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*this.fZoom, yIcon + 18*this.fZoom, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
+ oNode.text_attr['font-size'] = iFontSize * this.fZoom;
oText.attr(oNode.text_attr);
- oText.transform('s'+this.fZoom);
+ //oText.transform('s'+this.fZoom);
var oBB = oText.getBBox();
var dy = iHeight/2*this.fZoom + oBB.height/2;
oText.remove();
oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label);
oText.attr(oNode.text_attr);
- oText.transform('s'+this.fZoom);
+ //oText.transform('s'+this.fZoom);
oNode.aElements.push(oText);
oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}));
oText.toFront();
@@ -134,14 +146,15 @@ $(function()
}
oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr));
var oText = this.oPaper.text( xPos, yPos, oNode.label);
+ oNode.text_attr['font-size'] = iFontSize * this.fZoom;
oText.attr(oNode.text_attr);
- oText.transform('S'+this.fZoom);
+ //oText.transform('S'+this.fZoom);
var oBB = oText.getBBox();
var dy = iHeight/2*this.fZoom + oBB.height/2;
oText.remove();
oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
oText.attr(oNode.text_attr);
- oText.transform('S'+this.fZoom);
+ //oText.transform('S'+this.fZoom);
oNode.aElements.push(oText);
oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}).toBack());
break;
@@ -201,6 +214,21 @@ $(function()
oNode.ty += (oNode.y - oNode.yOrig) * this.fZoom;
oNode.xOrig = oNode.x;
oNode.yOrig = oNode.y;
+ this._updateBBox();
+ },
+ _updateBBox: function()
+ {
+ this.options.xmin = 9999;
+ this.options.xmax = -9999;
+ this.options.ymin = 9999;
+ this.options.ymax = -9999;
+ for(var k in this.aNodes)
+ {
+ this.options.xmin = Math.min(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmin);
+ this.options.xmax = Math.max(this.aNodes[k].x + this.aNodes[k].tx, this.options.xmax);
+ this.options.ymin = Math.min(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymin);
+ this.options.ymax = Math.max(this.aNodes[k].y + this.aNodes[k].ty, this.options.ymax);
+ }
},
_get_edge_path: function(oEdge)
{
@@ -281,7 +309,7 @@ $(function()
break;
case 'center':
- this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
+ this.xOffset = -xmin * this.fZoom + (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
break;
}
switch(this.options['vertical-align'])
@@ -295,15 +323,15 @@ $(function()
break;
case 'middle':
- this.yOffset = (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
+ this.yOffset = -ymin * this.fZoom + (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
break;
- }
-
-
+ }
},
add_node: function(oNode)
{
oNode.aElements = [];
+ oNode.tx = 0;
+ oNode.ty = 0;
this.aNodes.push(oNode);
},
add_edge: function(oEdge)
@@ -325,15 +353,15 @@ $(function()
{
var sPopupMenuId = 'tk_graph'+this.element.attr('id');
var sHtml = '';
this.element.before(sHtml);
@@ -377,7 +405,7 @@ $(function()
var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
me.show_group('relation_group_'+sGroupIndex);
},
- items: { 'show': {name: 'Show group' } }
+ items: { 'show': {name: me.options.drill_down.label } }
};
break;
@@ -387,16 +415,15 @@ $(function()
oResult = {
callback: function(key, options) {
var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
- var sURL = me.options.drill_down_url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
+ var sURL = me.options.drill_down.url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
window.location.href = sURL;
},
- items: { 'details': {name: 'Show Details' } }
+ items: { 'details': {name: me.options.drill_down.label } }
};
break;
default:
oResult = false; // No context menu
-
}
return oResult;
}
@@ -410,11 +437,27 @@ $(function()
{
oPositions[this.aNodes[k].id] = {x: this.aNodes[k].x, y: this.aNodes[k].y };
}
- var sHtmlForm = '