/**
* Constructs a Global. The constructor for this singleton should never be
* invoked directly.
*
* @classdesc
*
* A Global is the visualization in its entirety. It is a composition of
* {@link View}s and is responsible for coordinating information that's shared
* across Views. It is also responsible for drawing things shared across all
* Views such as the list of hidden hosts
*
* @constructor
*/
function Global($vizContainer, $sidebar, $hostBar, $logTable, views) {
if (!!Global.instance) {
throw new Exception("Global is a singleton - use getInstance() instead.");
}
/** @private */
this.views = views.slice();
/** @private */
this.viewL = this.views.length > 0 ? this.views[0] : null;
/** @private */
this.viewR = this.views.length > 1 ? this.views[1] : null;
/** @private */
this.hostPermutation = null;
/** @private */
this.controller = new Controller(this);
/** @private */
this.$vizContainer = $vizContainer;
/** @private */
this.$sidebar = $sidebar;
/** @private */
this.$hostBar = $hostBar;
/** @private */
this.searchbar = SearchBar.getInstance();
/** @private */
this.$logTable = $logTable;
/** @private */
this.showDiff = false;
/** @private */
this.pairwiseView = false;
this.$sidebar.css({
width: Global.SIDE_BAR_WIDTH + "px"
});
var context = this;
views.forEach(function(view) {
view.controller = context.controller; //TODO
});
}
/**
* @static
* @const
*/
Global.HOST_DIALOG_ID = "hostdialog";
/**
* @static
* @const
*/
Global.SIDE_BAR_WIDTH = 240;
/**
* @static
* @const
*/
Global.HOST_SIZE = 25;
/**
* @static
* @const
*/
Global.HOST_LABEL_WIDTH = 64;
/**
* @static
* @const
*/
Global.HIDDEN_EDGE_LENGTH = 40;
/**
* @static
* @const
*/
Global.MIN_HOST_WIDTH = 40;
/**
* @static
* @const
*/
Global.NODE_STROKE_COLOR = "white";
/**
* @static
* @const
*/
Global.NODE_STROKE_WIDTH = 2;
/**
* Redraws the global.
*/
Global.prototype.drawAll = function() {
var global = this;
var numViews = this.views.length;
var searchbar = this.searchbar;
this.resize();
this.$logTable.empty(); //TODO: check
this.$vizContainer.children("*").remove();
this.$hostBar.children("*").remove();
this.$vizContainer.height(global.getMaxViewHeight());
var viewLabelDiv = $('<div id="viewLabelDiv"></div>');
// Icons for left and right view, styled differently for labels and dropdowns
var labelIconL = $('<label id="labelIconL"></label>').text("r").hide();
var labelIconR = $('<label id="labelIconR"></label>').text("r").hide();
var selectIconL = $('<label id="selectIconL"></label>').text("r").hide();
var selectIconR = $('<label id="selectIconR"></label>').text("r").hide();
var leftLabel = this.viewL.getLabel();
var viewLabelL = $('<p id="viewLabelL"></p>').text(leftLabel).prepend(labelIconL);
var viewSelectDiv = $('<div id="viewSelectDiv"></div>');
var viewSelectL = $('<select id="viewSelectL"></select>');
// Show arrow icons and highlight cluster results that match a search term when on the Clusters tab
if ($(".leftTabLinks li").is(":visible") && $(".leftTabLinks li").last().hasClass("default")) {
if ($("#clusterNumProcess").is(":checked") || ($("#clusterComparison").is(":checked")
&& $(".clusterBase").find("option:selected").text() != "Select a base execution")) {
labelIconL.show(); selectIconL.show();
if (global.getPairwiseView()) {
labelIconR.show();
selectIconR.show();
}
var baseDropdown = $(".clusterBase");
var selected = $(".clusterBase option:selected");
var mode = searchbar.getMode();
// If the searchbar is not empty or in motif mode, fade out all executions in Clusters tab
if (mode != SearchBar.MODE_EMPTY) {
$("table.clusterResults a").filter(function() {
var text = $(this).text();
return text != "Show all" && text != "Condense";
}).addClass("execFade");
if (baseDropdown.is(":visible")) {
baseDropdown.addClass("fade");
}
// Remove fading for executions that match the search term
this.views.forEach(function(view) {
if (view.hasQueryMatch()) {
var label = view.getLabel();
if (baseDropdown.is(":visible") && selected.val() == label) {
baseDropdown.removeClass("fade");
} else {
$("table.clusterResults a").filter(function() {
return $(this).attr("href") == label;
}).removeClass("execFade");
}
}
});
}
}
}
if (this.viewR != null) {
// the "Pairwise" button is only visible when there are > 1 executions and when not doing a motif search
if (!$(".leftTabLinks li").first().next().hasClass("default") && searchbar.getMode() != SearchBar.MODE_MOTIF) {
$(".pairwiseButton").show();
}
$(".searchTabLinks li").last().show();
$(".visualization .left #tabs").css("height", "4.5em");
var rightLabel = this.viewR.getLabel();
// If there are only two executions in pairwise view, use labels instead of drop-downs
if (numViews == 2 && global.getPairwiseView()) {
this.$hostBar.append(viewLabelDiv);
var viewLabelR = $('<p id="viewLabelR"></p>').text(rightLabel).append(labelIconR);
viewLabelDiv.append(viewLabelL, viewLabelR);
// Otherwise, use drop-downs
} else {
this.$hostBar.append(viewSelectDiv);
var viewSelectR = $('<select id="viewSelectR"></select>').hide();
viewSelectDiv.append(selectIconL, selectIconR, viewSelectL);
if (numViews != 2) {
viewSelectDiv.append(viewSelectR);
}
this.views.forEach(function(view) {
var label = view.getLabel();
if (global.getPairwiseView()) {
if (label != rightLabel) {
viewSelectL.append('<option value="' + label + '">' + label + '</option>');
}
// Include every view as an option for the left drop-down when not in pairwise view
} else {
viewSelectL.append('<option value="' + label + '">' + label + '</option>');
}
if (label != leftLabel) {
viewSelectR.append('<option value="' + label + '">' + label + '</option>');
}
});
viewSelectL.children("option[value='" + leftLabel + "']").prop("selected", true);
viewSelectR.children("option[value='" + rightLabel + "']").prop("selected", true);
viewSelectR.unbind().on("change", function(e) {
var valR = $("#viewSelectR option:selected").val();
var valL = $("#viewSelectL option:selected").val();
global.controller.hideDiff();
if (searchbar.getMode() == SearchBar.MODE_MOTIF) {
if (global.getController().hasHighlight()) {
searchbar.clearResults();
}
searchbar.resetMotifResults();
}
global.viewR = global.getViewByLabel(valR);
if (global.getShowDiff()) {
global.controller.showDiff();
} else {
global.drawAll();
}
// When clustering, draw cluster icons next to the execution dropdown or label
if ($("#clusterNumProcess").is(":checked") || $("#clusterComparison").is(":checked")) {
global.drawClusterIcons();
}
});
}
}
// If there is a single execution
else {
// The label here is "" but it'll help shift the hostbar down
this.$hostBar.append(viewLabelDiv);
viewLabelDiv.append(viewLabelL);
$(".visualization .left #tabs").css("height", "2.5em");
}
viewSelectL.unbind().on("change", function(e) {
var valR = $("#viewSelectR option:selected").val();
var valL = $("#viewSelectL option:selected").val();
global.controller.hideDiff();
if (searchbar.getMode() == SearchBar.MODE_MOTIF) {
if (global.getController().hasHighlight()) {
searchbar.clearResults();
}
searchbar.resetMotifResults();
}
// If the selected view for viewL is the same as the current (hidden) viewR, change viewR to viewL so that
// when a user changes to pairwise view, viewL and viewR are not the same (this leads to only one view being drawn)
if (valL == global.viewR.getLabel()) {
global.viewR = global.viewL;
}
global.viewL = global.getViewByLabel(valL);
if (global.getShowDiff()) {
global.controller.showDiff();
} else {
global.drawAll();
}
// When clustering, draw cluster icons next to the execution dropdown or label
if ($("#clusterNumProcess").is(":checked") || $("#clusterComparison").is(":checked")) {
global.drawClusterIcons();
}
});
this.viewL.draw("L");
this.$vizContainer.append(this.viewL.getSVG());
this.$hostBar.append(this.viewL.getHostSVG());
this.$logTable.append(this.viewL.getLogTable());
this.controller.bindLines(this.viewL.getLogTable().find(".line:not(.more)"));
if (this.getPairwiseView()) {
$(".diffButton").show(); $("#viewSelectR").show();
this.viewR.draw("R");
// Draw the separator between the two views - this separator is only visible when
// at least one process is present (not hidden) in both views
if ((this.viewL.getTransformer().getHiddenHosts().length < this.viewL.getHosts().length) &&
(this.viewR.getTransformer().getHiddenHosts().length < this.viewR.getHosts().length)) {
var viewSeparator = $('<div id="viewSeparator"></div>');
viewSeparator.css("height", global.getMaxViewHeight());
this.$vizContainer.append(viewSeparator);
}
this.$vizContainer.append(this.viewR.getSVG());
this.$hostBar.append(this.viewR.getHostSVG());
this.$logTable.append($("<td></td>").addClass("spacer"));
this.$logTable.append(this.viewR.getLogTable());
this.controller.bindLines(this.viewR.getLogTable().find(".line:not(.more)"));
}
this.$vizContainer.height("auto");
$(".dialog").hide();
$(".hostConstraintDialog").hide();
this.drawSideBar();
// only call countMotifs if there are actually highlighted motifs to count,
// otherwise the motifGroup parameter to addMotif() in motifNavigator is null
if (this.getController().hasHighlightInView(this.viewL)) {
searchbar.countMotifs();
}
};
/**
* This function returns the height of the view with the larger/taller visualModel
*
* @returns {Number} the max height between the two active views
*/
Global.prototype.getMaxViewHeight = function() {
this.viewL.getVisualModel().update();
var maxHeight = this.viewL.getVisualModel().getHeight();
if (this.viewR != null) {
this.viewR.getVisualModel().update();
maxHeight = Math.max(maxHeight, this.viewR.getVisualModel().getHeight());
}
return maxHeight;
}
/**
* Gets the list of Views
*
* @returns {Array<View>} The list of views
*/
Global.prototype.getViews = function() {
return this.views.slice();
};
Global.prototype.getActiveViews = function() {
var result = [this.viewL];
if(this.viewR != null) {
result.push(this.viewR);
}
return result;
};
Global.prototype.getViewByLabel = function(label) {
for(var i = 0; i < this.views.length; i++) {
var view = this.views[i];
if(view.getLabel() == label) {
return view;
}
}
return null;
};
/**
* Sets the host permutation
* @param {HostPermutation} hostPermutation
*/
Global.prototype.setHostPermutation = function(hostPermutation) {
this.hostPermutation = hostPermutation;
};
/**
* Sets the showDiff boolean value
* @param {Boolean} showDiff
*/
Global.prototype.setShowDiff = function(showDiff) {
this.showDiff = showDiff;
}
/**
* Gets the showDiff boolean value
* @returns {Boolean} True if "Show Differences" was selected
*/
Global.prototype.getShowDiff = function() {
return this.showDiff;
}
/**
* Sets the pairwiseView boolean value
* @param {Boolean} pairwiseView
*/
Global.prototype.setPairwiseView = function(pairwiseView) {
this.pairwiseView = pairwiseView;
}
/**
* Gets the pairwiseView boolean value
* @returns {Boolean} True if "Pairwise" was selected
*/
Global.prototype.getPairwiseView = function() {
return this.pairwiseView;
}
/**
* Redraws the visualization with the two active views swapped
*/
Global.prototype.swapViews = function() {
// This checks to see that there are indeed two views to swap
if (this.viewL != null && this.viewR != null) {
var viewL = this.viewL;
this.viewL = this.viewR;
this.viewR = viewL;
this.drawAll();
// When clustering, draw cluster icons next to the execution dropdown or label
if ($("#clusterNumProcess").is(":checked") || $("#clusterComparison").is(":checked")) {
this.drawClusterIcons();
}
}
}
/**
* Sets the view in the given position to the execution with the label specified by anchorHref
*
* @param {String} position Either "L" or "R" to indicate the left or right view
* @param {String} anchorHref The label of the execution to be set
*/
Global.prototype.setView = function(position, anchorHref) {
var leftView = this.getActiveViews()[0].getLabel();
var rightView = this.getActiveViews()[1].getLabel();
if ((position == "L" && anchorHref == rightView) || (position == "R" && this.getPairwiseView() && anchorHref == leftView)) {
this.swapViews();
} else {
// For logs with exactly two executions, there are no execution drop-downs so have to call drawClusterIcons directly
if (this.getPairwiseView() && this.getViews().length == 2) {
this.drawClusterIcons();
} else {
if (position == "L") {
$("#viewSelectL").children("option[value='" + anchorHref + "']").prop("selected", true).change();
} else {
$("#viewSelectR").children("option[value='" + anchorHref + "']").prop("selected", true).change();
}
}
}
}
/**
* Gets the {@link Controller}
* @returns {Controller} The controller
*/
Global.prototype.getController = function() {
return this.controller;
};
/**
* Resizes the graph
*/
Global.prototype.resize = function() {
var viewLNumHosts = getNumVisibleHosts(this.viewL.getHosts(), this.viewL.getTransformer().getSpecifiedHiddenHosts());
var viewRNumHosts = 0;
if (this.viewR != null && this.getPairwiseView()) {
viewRNumHosts = getNumVisibleHosts(this.viewR.getHosts(), this.viewR.getTransformer().getSpecifiedHiddenHosts());
}
var visibleHosts = viewLNumHosts + viewRNumHosts;
// TODO: rename to sidebarLeft sidebarRight middleWidth
var headerWidth = $(".visualization header").outerWidth();
var sidebarWidth = this.$sidebar.outerWidth();
var globalWidth = $(window).width() - headerWidth - sidebarWidth;
$("#searchbar").width(globalWidth);
var widthPerHost = Math.max(Global.MIN_HOST_WIDTH, globalWidth / visibleHosts);
var logTableWidth = this.viewR != null && this.getPairwiseView() ? (Global.SIDE_BAR_WIDTH - 12) / 2 : Global.SIDE_BAR_WIDTH;
this.viewL.setWidth(viewLNumHosts * widthPerHost);
this.viewL.setLogTableWidth(logTableWidth);
if (this.viewR != null && this.getPairwiseView()) {
this.viewR.setWidth(viewRNumHosts * widthPerHost);
this.viewR.setLogTableWidth(logTableWidth);
}
var sel = d3.select("circle.sel").data()[0];
if (sel) {
var $svg = $(d3.select("circle.sel").node()).parents("svg");
var $dialog = $(".dialog");
if (sel.getX() > $svg.width() / 2)
$dialog.css({
"left": sel.getX() + $svg.offset().left + 40
}).removeClass("right").addClass("left").show();
else
$dialog.css({
"left": sel.getX() + $svg.offset().left - $dialog.width() - 40
}).removeClass("left").addClass("right").show();
}
function getNumVisibleHosts(allHosts, hiddenHosts) {
var hostSet = {};
allHosts.forEach(function(host) {
hostSet[host] = true;
});
hiddenHosts.forEach(function(host) {
delete hostSet[host];
});
var count = 0;
for(var key in hostSet) {
count++;
}
return count;
}
};
/**
* Draws a normal, rectangular hidden host
*
* @param {d3.selection} container The selection to append the new element to
* @returns {d3.selection} The new selection containing the appended rectangle
*/
Global.prototype.drawHiddenHost = function(container) {
var hiddenHost = container.append("rect");
return hiddenHost;
}
/**
* Draws a unique, diamond hidden host
*
* @param {d3.selection} container The selection to append the new element to
* @returns {d3.selection} The new selection containing the appended polygon
*/
Global.prototype.drawHiddenHostAsRhombus = function(container) {
var hiddenHost = container.append("polygon");
return hiddenHost;
}
/**
* Draws arrow icons next to the base dropdown or next to execution labels in the Clusters tab.
*/
Global.prototype.drawClusterIcons = function() {
$("#clusterIconL, #clusterIconR, br.spaceL, br.spaceR, br.left, br.right").remove();
var leftLabel = this.viewL.getLabel();
var rightLabel = this.viewR.getLabel();
var clusterIconL = $('<label id="clusterIconL"></label>').text("r");
var clusterIconR = $('<label id="clusterIconR"></label>').text("r");
var leftLink = $("table.clusterResults a").filter(function() { return $(this).attr("href") == leftLabel; });
var rightLink = $("table.clusterResults a").filter(function() { return $(this).attr("href") == rightLabel; });
// Set margin for base dropdown to zero initially
$(".clusterBase").removeClass("baseIndent");
// If the selected execution is hidden in a condensed list, click the Show all button to make it visible
if (!leftLink.is(":visible")) {
$(leftLink.nextAll("a").filter(function() {
return $(this).text() == "Show all";
})[0]).click();
}
if (!rightLink.is(":visible") && this.getPairwiseView()) {
$(rightLink.nextAll("a").filter(function() {
return $(this).text() == "Show all";
})[0]).click();
}
// If the left graph is the specified base execution, draw the left arrow icon next to the dropdown
if ($("#clusterComparison").is(":checked")) {
if (leftLabel == $("select.clusterBase").val()) {
$("#baseLabel").after($("<br class='spaceL'>").hide());
$(".clusterBase").before(clusterIconL.addClass("baseIcon")).addClass("baseIndent");
if (this.getPairwiseView()) {
$(rightLink.before(clusterIconR).next()).after($("<br class=right>").hide());;
}
// If the right graph is the specified base execution, draw the right arrow icon next to the dropdown
} else if (this.getPairwiseView() && rightLabel == $("select.clusterBase").val()) {
$("#baseLabel").after($("<br class='spaceR'>").hide());
$(".clusterBase").before(clusterIconR.addClass("baseIcon")).addClass("baseIndent");
$(leftLink.before(clusterIconL).next()).after($("<br class=left>").hide());
}
}
// Otherwise, draw the appropriate arrow beside the correct execution label
$(leftLink.before(clusterIconL).next()).after($("<br class=left>").hide());
if (this.getPairwiseView()) {
$(rightLink.before(clusterIconR).next()).after($("<br class=right>").hide());
}
}
/**
* Draws the hidden hosts, if any exist.
*
* @private
*/
Global.prototype.drawSideBar = function() {
var global = this;
this.$sidebar.children("#hiddenHosts").remove();
this.$sidebar.children("#viewSelectDiv").remove();
// Draw hidden hosts
var hiddenHosts = {};
this.viewL.getTransformer().getHiddenHosts().forEach(function(host) {
hiddenHosts[host] = true;
});
if (this.viewR != null && this.getPairwiseView()) {
this.viewR.getTransformer().getHiddenHosts().forEach(function(host) {
hiddenHosts[host] = true;
});
}
var hh = Object.keys(hiddenHosts);
if (hh.length <= 0) {
return;
}
this.$sidebar.append('<div id="hiddenHosts">Hidden processes:</div>');
var hiddenHostsSelection = d3.select("#hiddenHosts");
var hiddenHostsSVG = hiddenHostsSelection.append("svg");
var hostsPerLine = Math.floor((Global.SIDE_BAR_WIDTH + 5) / (Global.HOST_SIZE + 5));
hiddenHostsSVG
.attr("width", this.$sidebar.width())
.attr("height", Math.ceil(hh.length / hostsPerLine) * (Global.HOST_SIZE + 5) - 5)
.attr("class", "hidden-hosts");
var hiddenHostsGroup = hiddenHostsSVG.append("g");
hiddenHostsGroup.append("title").text("Double click to view");
var first = true; var count = 0;
// initial points for a unique host (ie. x and y coordinates for each corner of the rhombus shape)
var x1 = 12; var y1 = 0; var x2 = 22; var y2 = 12;
var x3 = 12; var y3 = 24; var x4 = 2; var y4 = 12;
// initial x and y coordinates for a normal host
var rectx = 0; var recty = 0;
hh.forEach(function(host) {
var hiddenHost = global.drawHiddenHost(hiddenHostsSVG);
// If showDiff is true, check if this hidden host needs to be drawn as a rhombus
if (global.getShowDiff()) {
var uniqueHostsL = global.viewL.getTransformer().getUniqueHosts();
//check if this hidden host is in the list of unique hosts for viewL
if (uniqueHostsL && uniqueHostsL.indexOf(host) != -1) {
hiddenHost = global.drawHiddenHostAsRhombus(hiddenHostsSVG);
}
else if (global.viewR != null && global.getPairwiseView()) {
//check if this hidden host is in the list of unique hosts for viewR
var uniqueHostsR = global.viewR.getTransformer().getUniqueHosts();
if (uniqueHostsR && uniqueHostsR.indexOf(host) != -1) {
hiddenHost = global.drawHiddenHostAsRhombus(hiddenHostsSVG);
}
}
}
hiddenHost.attr("width", Global.HOST_SIZE);
hiddenHost.attr("height", Global.HOST_SIZE);
hiddenHost.style("fill", global.hostPermutation.getHostColor(host));
hiddenHost.append("title").text("Double click to view");
// start over on a new line once the hidden hosts have taken up the side bar width
if (count == hostsPerLine) {
if (global.getShowDiff()) {
x1 = 12; y1 += Global.HOST_SIZE + 5;
x2 = 22; y2 += Global.HOST_SIZE + 5;
x3 = 12; y3 += Global.HOST_SIZE + 5;
x4 = 2; y4 += Global.HOST_SIZE + 5;
}
rectx = 0; recty += Global.HOST_SIZE + 5;
first = true;
count = 0;
}
// increment x coordinates so that the next hidden host will be drawn
// next to the currently hidden hosts without any overlap
if (!first) {
if (global.getShowDiff()) {
x1 += Global.HOST_SIZE + 5;
x2 += Global.HOST_SIZE + 5;
x3 += Global.HOST_SIZE + 5;
x4 += Global.HOST_SIZE + 5;
}
rectx += Global.HOST_SIZE + 5;
}
first = false;
// update attributes of the drawn node
if (global.getShowDiff()) {
var points = [x1,y1,x2,y2,x3,y3,x4,y4];
hiddenHost.attr("points", points.join());
}
hiddenHost.attr("x", rectx);
hiddenHost.attr("y", recty);
count++;
// bind the hidden host nodes to user input
global.controller.bindHiddenHosts(host, hiddenHost);
});
};