/**
* Constructs a Controller to control the given {@link Global}
*
* @classdesc
*
* The Controller manipulates the model on user input. It is responsible for
* maintaining {@link Transformation}s.
*
* @constructor
*/
function Controller(global) {
/** @private */
this.global = global;
var self = this;
var searchbar = SearchBar.getInstance();
self.bindScroll();
// $(window).unbind("scroll");
// $(window).bind("scroll", self.onScroll);
// $(window).scroll();
$(window).unbind("resize");
$(window).on("resize", function() {
try {
self.global.drawAll();
}
catch (exception) {
Shiviz.getInstance().handleException(exception);
}
});
$(window).unbind("keydown.dialog").on("keydown.dialog", function(e) {
if (e.which == 27) {
$(".dialog").hide();
d3.select("circle.sel").each(function(d) {
$(this).remove();
d.setSelected(false);
});
}
self.bindScroll();
});
$(window).unbind("click.dialog").on("click.dialog", function(e) {
var $target = $(e.target);
var tn = $target.prop("tagName");
// Test for click inside dialog
if ($target.is(".dialog") || $target.parents(".dialog").length)
return;
// Test for node or host click
if (tn == "g" || $target.parents("g").length || $target.parents(".hidden-hosts").length)
return;
// Test for line click
if ($target.parents(".log").length || $target.is(".highlight"))
return;
// Test for clickable
if (tn.match(/input/i) || tn.match(/button/i))
return;
// Test for panel visibility
if ($("#searchbar #panel:visible").length)
return;
$(".dialog").hide();
// remove the scrolling behavior for hiding/showing dialog boxes once we click outside the box
$(window).unbind("scroll");
d3.select("circle.sel").each(function(d) {
$(this).remove();
d.setSelected(false);
});
d3.select("polygon.sel").each(function(d) {
$(this).remove();
d.setSelected(false);
});
self.bindScroll();
});
$("#searchbar #panel").unbind("click").on("click", function(e) {
var $target = $(e.target);
// Test for click inside a host square or a constraint dialog
if ($target.is("rect") || $target.parents(".hostConstraintDialog").length)
return;
if ($target.is("text"))
return;
$(".hostConstraintDialog").hide();
self.bindScroll();
});
$(".dialog button").unbind().click(function() {
var type = this.name;
var e = $(".dialog").data("element");
switch (type) {
// Hide host
case "hide":
self.hideHost(e);
break;
// Unhide host
case "unhide":
self.unhideHost(e);
break;
// Highlight host
case "filter":
self.toggleHostHighlight(e);
break;
// Toggle collapse
case "collapse":
self.toggleCollapseNode(e);
break;
}
self.bindScroll();
});
$(".diffButton").unbind().click(function() {
// remove the scrolling behavior for hiding/showing dialog boxes when the diff button is clicked
$(window).unbind("scroll");
$(this).toggleClass("fade");
if ($(this).text() == "Show Differences") {
$(this).text("Hide Differences");
global.setShowDiff(true);
self.showDiff();
}
else {
$(this).text("Show Differences");
global.setShowDiff(false);
self.hideDiff();
}
self.bindScroll();
});
$(".pairwiseButton").unbind().click(function() {
// remove the scrolling behavior for hiding/showing dialog boxes when the pairwise button is clicked
$(window).unbind("scroll");
$(this).toggleClass("fade");
if ($(this).text() == "Pairwise") {
$(this).text("Individual");
global.setPairwiseView(true);
global.drawAll();
if ($("#clusterNumProcess").is(":checked") || $("#clusterComparison").is(":checked")) {
global.drawClusterIcons();
}
}
else {
$(this).text("Pairwise");
// Remove the right view arrow when viewing graphs individually
$("table.clusterResults #clusterIconR").remove();
// Remove differences when viewing graphs individually
if (global.getShowDiff()) {
$(".diffButton").click();
}
$(".diffButton").hide();
global.setPairwiseView(false);
global.drawAll();
if ($("#clusterNumProcess").is(":checked") || $("#clusterComparison").is(":checked")) {
global.drawClusterIcons();
}
}
self.bindScroll();
});
// Event handler for switching between the left tabs
$(".visualization .leftTabLinks a").unbind().on("click", function(e) {
var anchorHref = $(this).attr("href");
$(".visualization #" + anchorHref).show().siblings().hide();
$(this).parent("li").addClass("default").siblings("li").removeClass("default");
$("#labelIconL, #labelIconR, #selectIconL, #selectIconR").hide();
if (anchorHref != "logTab") {
// Remove any log line highlighting when not on the Log lines tab
$(".highlight").css("opacity", 0);
}
if (anchorHref == "clusterTab") {
// Clear all motif results when on the clusters tab
if (searchbar.getMode() == SearchBar.MODE_MOTIF) {
if (global.getController().hasHighlight()) {
searchbar.clearResults();
}
searchbar.resetMotifResults();
}
if ($("#clusterNumProcess").is(":checked") || ($("#clusterComparison").is(":checked") && $(".clusterBase").find("option:selected").text() != "Select a base execution")) {
$("#labelIconL, #selectIconL").show();
if (global.getPairwiseView()) {
$("#labelIconR, #selectIconR").show();
}
}
}
// Show the pairwise button for log lines and clusters when not doing a motif search
if (global.getViews().length > 1 && searchbar.getMode() != SearchBar.MODE_MOTIF) {
$(".pairwiseButton").show();
}
if (anchorHref == "motifsTab") {
if (global.getPairwiseView()) {
$(".pairwiseButton").click();
}
$(".pairwiseButton").hide();
if ($(".motifResults a").length > 0) {
searchbar.setValue("#motif");
}
}
e.preventDefault();
self.bindScroll();
});
// Event handler for switching between clustering options
$("#clusterNumProcess, #clusterComparison").unbind().on("change", function() {
$("#labelIconL, #labelIconR, #selectIconL, #selectIconR").hide();
$("#clusterIconL, #clusterIconR").remove();
if ($(this).is(":checked")) {
$(this).siblings("input").prop("checked", false);
// Generate clustering results
var clusterMetric = $(this).attr("id");
var clusterer = new Clusterer(clusterMetric, global);
clusterer.cluster();
} else {
// Clear the results if no option is selected
$(".clusterResults td.lines").empty();
$(".clusterResults td:empty").remove();
$("#baseLabel, .clusterBase").hide();
}
self.bindScroll();
});
}
/**
* Highlights a motif across all {@link View}s using the provided motif finder.
* The visualization is then re-drawn.
*
* @param {MotifFinder} motifFinder
* @see {@link HighlightMotifTransformation}
*/
Controller.prototype.highlightMotif = function(motifFinder) {
this.global.getViews().forEach(function(view) {
view.getTransformer().highlightMotif(motifFinder, false);
});
this.global.drawAll();
this.bindScroll();
};
/**
* Clears highlighting of motifs across all {@link View}s. The visualization is
* then re-drawn
*
* @see {@link HighlightMotifTransformation}
*/
Controller.prototype.clearHighlight = function() {
this.global.getViews().forEach(function(view) {
view.getTransformer().unhighlightMotif();
});
this.global.drawAll();
this.bindScroll();
};
/**
* Determines if a motif is being highlighted in any of the {@link View}s.
*
* @returns {Boolean} True if a motif is being highlighted
*/
Controller.prototype.hasHighlight = function() {
var views = this.global.getViews();
for (var i = 0; i < views.length; i++) {
if (views[i].getTransformer().hasHighlightedMotif()) {
return true;
}
}
return false;
};
/**
* Determines if a motif is being highlighted in the given View
*
* @param {View} view
* @returns {Boolean} True if a motif is being highlighted in the view
*/
Controller.prototype.hasHighlightInView = function(view) {
if (view.getTransformer().hasHighlightedMotif()) {
return true;
} else {
return false;
}
}
/**
* Hides the specified host across all {@link View}s. The visualization is then
* re-drawn.
*
* @param {String} host The host to hide.
*/
Controller.prototype.hideHost = function(host) {
$(window).unbind("scroll");
this.global.getViews().forEach(function(view) {
view.getTransformer().hideHost(host);
});
this.global.drawAll();
this.bindScroll();
};
/**
* Unhides the specified host across all {@link View}s. The visualization is
* then re-drawn.
*
* @param {String} host The host to unhide.
*/
Controller.prototype.unhideHost = function(host) {
$(window).unbind("scroll");
this.global.getViews().forEach(function(view) {
view.getTransformer().unhideHost(host);
});
this.global.drawAll();
this.bindScroll();
};
/**
* Toggles the highlighting of a host across all {@link View}s. The
* visualization is then re-drawn.
*
* @param {String} host The host whose highlighting is to be toggled
*/
Controller.prototype.toggleHostHighlight = function(host) {
this.global.getViews().forEach(function(view) {
view.getTransformer().toggleHighlightHost(host);
});
this.global.drawAll();
this.bindScroll();
};
/**
* Toggles the collapsing of a node.
*
* @param {ModelNode} node
*/
Controller.prototype.toggleCollapseNode = function(node) {
$(window).unbind("scroll");
this.global.getViews().forEach(function(view) {
view.getTransformer().toggleCollapseNode(node);
});
this.global.drawAll();
this.bindScroll();
};
/**
* Highlights different hosts among the current active views
* This method should only be called when there are > 1 execution
* and graphs are displayed pairwise
* @see {@link ShowDiffTransformation}
*/
Controller.prototype.showDiff = function() {
var views = this.global.getActiveViews();
var viewL = views[0];
var viewR = views[1];
viewL.getTransformer().showDiff(viewR);
viewR.getTransformer().showDiff(viewL);
this.global.drawAll();
this.bindScroll();
};
/**
* Re-draws the graph to not highlight different hosts
* This method should only be called when there are > 1 execution
* and graphs are displayed pairwise
* @see {@link ShowDiffTransformation}
*/
Controller.prototype.hideDiff = function() {
var views = this.global.getActiveViews();
var viewL = views[0];
var viewR = views[1];
viewL.getTransformer().hideDiff(viewR);
viewR.getTransformer().hideDiff(viewL);
this.global.drawAll();
this.bindScroll();
};
/**
* Binds events to the nodes.
*
* <ul>
* <li>mouseover: highlights node & log line, shows info in sidebar</li>
* <li>shift + click: toggles collapsed node</li>
* </ul>
*
* @param {d3.selection} nodes A D3 selection of the nodes.
*/
Controller.prototype.bindNodes = function(nodes) {
var controller = this;
nodes.on("click", function(e) {
if (d3.event.shiftKey) {
// Toggle node collapsing
controller.toggleCollapseNode(e.getNode());
}
else {
controller.showDialog(e, 0, this);
}
}).on("mouseover", function(e) {
d3.selectAll("g.focus .sel").transition().duration(100)
.attr("r", function(d) {
return d.getRadius() + 4;
});
d3.selectAll("g.focus").classed("focus", false).select("circle:not(.sel)").transition().duration(100)
.attr("r", function(d) {
return d.getRadius();
});
d3.select(this).classed("focus", true).select("circle:not(.sel)").transition().duration(100)
.attr("r", function(d) {
return d.getRadius() + 2;
});
d3.selectAll("g.focus .sel").transition().duration(100)
.attr("r", function(d) {
return d.getRadius() + 6;
});
$(".event").text(e.getText());
$(".fields").children().remove();
if (!e.isCollapsed()) {
var fields = e.getNode().getLogEvents()[0].getFields();
for (var i in fields) {
var $f = $("<tr>", {
"class": "field"
});
var $t = $("<th>", {
"class": "title"
}).text(i + ":");
var $v = $("<td>", {
"class": "value"
}).text(fields[i]);
$f.append($t).append($v);
$(".fields").append($f);
}
}
$(".line.focus").css({
"color": $(".focus").data("fill"),
"background": "",
"width": "inherit"
}).removeClass("focus");
$(".reveal").removeClass("reveal");
var $line = $("#line" + e.getId());
var $parent = $line.parent(".line").addClass("reveal");
// Only highlight log lines on the Log Lines tab
if ($(".leftTabLinks li").first().hasClass("default")) {
$line.addClass("focus").css({
"background": "transparent",
"color": "white",
"width": "calc(" + $line.width() + "px - 1em)"
}).data("fill", e.getFillColor());
$(".highlight").css({
"width": $line.width(),
"height": $line.height(),
"opacity": e.getOpacity()
});
var top = parseFloat($line.css("top")) || 0;
var ptop = parseFloat($parent.css("top")) || 0;
var margin = parseFloat($line.css("margin-top")) || 0;
var pmargin = parseFloat($parent.css("margin-top")) || 0;
var vleft = $(".visualization .left").offset().left;
var vtop = $(".visualization .left").offset().top;
var offset = $(".log").offset().top - vtop;
$(".highlight").css({
"background": e.getFillColor(),
"top": top + ptop + margin + pmargin + offset,
"left": $line.offset().left - parseFloat($line.css("margin-left")) - vleft
}).attr({
"data-ln": e.getLineNumber()
}).data({
"id": e.getId()
}).show();
}
});
};
/**
* Binds events to hosts
*
* <ul>
* <li>double-click: Hides the host</li>
* <li>shift+double-click: Highlights the host</li>
* </ul>
*
* @param {d3.selection} hosts A D3 selection of the host rects
*/
Controller.prototype.bindHosts = function(hosts) {
var controller = this;
hosts.on("mouseover", function(e) {
$(".event").text(e.getText());
$(".fields").children().remove();
}).on("dblclick", function(e) {
var views = controller.global.getViews();
if (d3.event.shiftKey) {
// Filtering by host
// If more than one view / execution then return
if (views.length != 1)
return;
controller.toggleHostHighlight(e.getHost());
}
else {
// Hide host
controller.hideHost(e.getHost());
}
}).on("click", function(e) {
controller.showDialog(e, 1, this);
});
};
/**
* Binds node highlighting to mouseover event on log lines
*
* @param {jQuery.selection} lines A jQuery selection of the log lines
*/
Controller.prototype.bindLines = function(lines) {
lines.unbind().on("mouseover", function() {
var id = "#node" + $(this).data("id");
$(id)[0].dispatchEvent(new MouseEvent("mouseover"));
})
lines.add(".highlight").on("click", function() {
var id = "#node" + $(this).data("id");
$(id)[0].dispatchEvent(new MouseEvent("click"));
});
};
/**
* Binds unhide to double-click event on hidden hosts.
*
* @param {String} host The host that is hidden
* @param {d3.selection} node The visualNode that was hidden
*/
Controller.prototype.bindHiddenHosts = function(host, node) {
var controller = this;
node.on("dblclick", function(e) {
controller.unhideHost(host);
}).on("mouseover", function(e) {
$(".event").text(host);
$(".fields").children().remove();
}).on("click", function(e) {
controller.showDialog(host, 2, this);
});
};
/**
* Ensures things are positioned correctly on scroll
*
* @private
* @param {Event} e The event object JQuery passes to the handler
*/
Controller.prototype.onScroll = function(e) {
var x = window.pageXOffset;
$("#hostBar, .dialog.host:not(.hidden)").css("margin-left", -x);
$(".log").css("margin-left", x);
if ($(".line.focus").length) {
$(".highlight").css({
"left": $(".line.focus").offset().left - parseFloat($(".line.focus").css("margin-left")) - $(".visualization .left").offset().left
});
}
this.toggleGreyHostNodes();
};
/**
* Shows the node selection popup dialog
*
* @param {VisualNode} e The VisualNode that is selected
* @param {Number} type The type of node: 0 for regular, 1 for host, 2 for
* hidden host
* @param {DOMElement} elem The SVG node element
*/
Controller.prototype.showDialog = function(e, type, elem) {
const controller = this;
// Remove existing selection highlights
d3.select("circle.sel").each(function(d) {
$(this).remove();
d.setSelected(false);
});
d3.select("polygon.sel").each(function(d) {
$(this).remove();
d.setSelected(false);
});
// Highlight the node with an appropriate outline
if (!type) {
e.setSelected(true);
var id = e.getId();
var views = this.global.getActiveViews();
// If showDiff is true, check if the selected node should be outlined with a rhombus
if (this.global.getShowDiff()) {
var uniqueEventsL = views[0].getTransformer().getUniqueEvents();
var uniqueEventsR = views[1].getTransformer().getUniqueEvents();
// If this node is not a unique event, highlight the node with a circular outline
if (uniqueEventsL.indexOf(id) == -1 && uniqueEventsR.indexOf(id) == -1) {
var selcirc = d3.select("#node" + e.getId()).insert("circle", "circle");
selcirc.style("fill", function(d) {
return d.getFillColor();
});
selcirc
.attr("class", "sel")
.attr("r", function(d) {
return d.getRadius() + 6;
});
// If this node is a unique event, highlight it with a rhombus outline
} else {
var selrhombus = d3.select("#node" + e.getId()).insert("polygon", "polygon");
selrhombus
.style("stroke", function(d) { return d.getFillColor(); })
.style("stroke-width", 2)
.style("fill", "white");
selrhombus
.attr("class", "sel")
.attr("points", function(d) {
var points = d.getPoints();
var newPoints = [points[0], points[1]-3, points[2]+3,
points[3], points[4], points[5]+3, points[6]-3, points[7]];
return newPoints.join();
});
}
// If showDiff is false, all node outlines are circular
} else {
var selcirc = d3.select("#node" + e.getId()).insert("circle", "circle");
selcirc
.style("fill", function(d) {
return d.getFillColor();
});
selcirc
.attr("class", "sel")
.attr("r", function(d) {
return d.getRadius() + 6;
});
}
}
const $rect = $(elem).is("rect") ? $(elem) : $(elem).find("rect");
var $svg = $rect.parents("svg");
var $dialog = $(".dialog");
var $graph = $("#graph");
// Set properties for dialog, and show
if (type == 2)
$dialog.css({
"left": $rect.offset().left - $dialog.width() - 40
}).removeClass("left").addClass("right").show();
else if (e.getX() - $(window).scrollLeft() > $graph.width() / 2)
$dialog.css({
"left": e.getX() + $svg.offset().left - $dialog.width() - 40,
"margin-left": type ? -$(window).scrollLeft() : 0
}).removeClass("left").addClass("right").show();
else
$dialog.css({
"left": e.getX() + $svg.offset().left + 40,
"margin-left": type ? -$(window).scrollLeft() : 0
}).removeClass("right").addClass("left").show();
// Set fill color, etc.
if (type) {
const rectOffset = $rect.offset().top;
const scrollOffset = $(window).scrollTop();
const hostAdjust = Global.HOST_SIZE / 2;
const top = rectOffset - scrollOffset + hostAdjust;
$dialog.css({
"top": top,
"background": type == 2 ? $rect.css("fill") : e.getFillColor(),
"border-color": type == 2 ? $rect.css("fill") : e.getFillColor()
}).data("element", type == 2 ? e : e.getHost());
} else {
$dialog.css({
"top": e.getY() + $svg.offset().top,
"background": e.getFillColor(),
"border-color": e.getFillColor()
}).data("element", e.getNode());
}
// Set class "host" if host (hidden or not) is selected
if (type) {
$dialog.addClass("host");
if (type == 2) {
$dialog.addClass("hidden");
} else {
$dialog.removeClass("hidden");
}
} else {
$dialog.removeClass("host");
}
// Add info to the dialog
$dialog.find(".name").text(type == 2 ? e : e.getText());
$dialog.find(".info").children().remove();
if (!type && !e.isCollapsed()) {
// Add fields, if normal node
var fields = e.getNode().getLogEvents()[0].getFields();
for (var i in fields) {
var $f = $("<tr>", {
"class": "field"
});
var $t = $("<th>", {
"class": "title"
}).text(i + ":");
var $v = $("<td>", {
"class": "value"
}).text(fields[i]);
$f.append($t).append($v);
$dialog.find(".info").append($f);
}
// Hide highlight button
$dialog.find(".filter").hide();
// If node is collapsible then show collapse button
// Else don't show button
if (!CollapseSequentialNodesTransformation.isCollapseable(e.getNode(), 2))
$dialog.find(".collapse").hide();
else
$dialog.find(".collapse").show().text("Collapse");
}
else if (!type) {
// Show uncollapse button for collapsed nodes
$dialog.find(".collapse").show().text("Expand");
$dialog.find(".filter").hide();
}
else {
// Show highlight button if only one execution
if (type == 2 || this.global.getViews().length > 1)
$dialog.find(".filter").hide();
else
$dialog.find(".filter").show();
// Set highlight/unhighlight based on current state
if (type != 2 && e.isHighlighted())
$dialog.find(".filter").text("Unfilter");
else
$dialog.find(".filter").text("Filter");
// Set hide/unhide based on state
if (type == 2)
$dialog.find(".hide").attr("name", "unhide").text("Unhide");
else
$dialog.find(".hide").attr("name", "hide").text("Hide");
// Hide collapse button
$dialog.find(".collapse").hide();
}
// This is necessary to set so that host dialogs can be identified.
// Dialog visibility toggling should ignore host dialogs only.
if (type === 1) {
// It is a host dialog
$dialog.attr("id", Global.HOST_DIALOG_ID);
} else {
$dialog.attr("id", "");
}
// The dialogtop doesn't change while scrolling, but it does get set
// to 0 by JS when hidden, so must keep track of its location
const visibleDialogTop = $dialog.offset().top;
$(window).scroll(function() {
toggleDialogVisibility();
controller.toggleGreyHostNodes();
});
// Makes the dialog disappear if it scrolls above the bottom of the
// hostbar; or reappear if scrolls lower than it. Does nothing
// to host node dialogs
function toggleDialogVisibility() {
// After scrolling, the hostbarbottom offset will have changed. We
// will use this latest version to determine if the dialog
// is above it or not.
const scrolledHostbarBottom = getHostbarBottomOffset();
const isHostDialog = $dialog.attr("id") === Global.HOST_DIALOG_ID;
if (isHostDialog) {
// do nothing
} else if (scrolledHostbarBottom > visibleDialogTop) {
// dialog is above hostbar
$dialog.hide();
} else {
// dialog is below hostbar
$dialog.show();
}
}
}
// If scrolling past a host's last process, grey out host
Controller.prototype.toggleGreyHostNodes = function () {
for (let view of this.global.getViews()) {
for (let visualNode of view.getTailNodes()) {
const isScrolledPast = isAboveHostbar(visualNode);
view.setGreyHost(visualNode, isScrolledPast);
}
}
// VisualNode => Boolean
function isAboveHostbar(visualNode) {
const $circle = visualNode.getSVG().find("circle");
if ($circle.length > 0) {
const circleTop = $circle.offset().top;
const hostBarBottom = getHostbarBottomOffset();
return circleTop < hostBarBottom;
} else {
return true;
}
}
};
Controller.prototype.bindScroll = function(){
var self = this;
$(window).unbind("scroll");
$(window).bind("scroll", function(e) {
self.onScroll(e);
});
$(window).scroll();
}
// Return current offset of the hostbar. This gets larger the lower down we
// scroll
function getHostbarBottomOffset() {
const $hostbar = $("#hostBar");
if ($hostbar.length > 0) {
const hostbarBottom = $hostbar.offset().top + $hostbar.height()
+ parseFloat($hostbar.css('padding-top'));
return hostbarBottom;
} else {
return 0;
}
}