/**
* Constructs a VisualNode given a {@link ModelNode}. The newly constructed
* VisualNode will represent the visualization of the {@link ModelNode}.
*
* @classdesc
*
* A VisualNode represents the visualization of an {@link ModelNode} that is,
* this class describes how the Node should be drawn (such as its size, color,
* etc). Note that the actual drawing logic is not part of this class.
*
* @constructor
* @param {ModelNode} node The node to associate with this VisualNode. This
* object will then be a visualization of the argument
*/
function VisualNode(node) {
/** @private */
this.id = VisualNode.id++;
/** @private */
this.node = node;
/** @private */
this.$svg = Util.svgElement("g");
this.$title = $("<title></title>");
this.$circle = Util.svgElement("circle");
this.$text = Util.svgElement("text");
this.$hiddenParentLine = Util.svgElement("line");
this.$hiddenChildLine = Util.svgElement("line");
this.$rect = Util.svgElement("rect");
this.$diamond = Util.svgElement("polygon");
this.$highlightRect = Util.svgElement("rect");
/** @private */
this.x = 0;
/** @private */
this.y = 0;
this.setX(0);
this.setY(0);
/** @private */
this.radius = 0;
this.setRadius(5);
/** @private */
this.points = [0,0,0,0,0,0,0,0];
/** @private */
this.fillColor;
this.setFillColor("#000");
/** @private */
this.strokeColor;
this.setStrokeColor(Global.NODE_STROKE_COLOR);
/** @private */
this.strokeWidth;
this.setStrokeWidth(Global.NODE_STROKE_WIDTH);
/** @private */
this.opacity;
this.setOpacity(1);
/** @private */
this.label = "";
this.setLabel("");
/** @private */
this.hasHiddenParentInner = false;
/** @private */
this.hasHiddenChildInner = false;
/** @private */
this._isHighlighted = false;
/** @private */
this._isSelected = false;
if (this.isStart()) {
this.$rect.attr({
"width": Global.HOST_SIZE,
"height": Global.HOST_SIZE
});
this.$svg.append(this.$rect);
this.$highlightRect.attr({
"fill": "transparent",
"stroke": "#FFF",
"stroke-width": "2px",
"pointer-events": "none"
});
} else {
this.$title.text(this.getText());
this.$svg.append(this.$title);
var mouseOverRect = Util.svgElement("rect");
mouseOverRect.attr({
"width": 48,
"height": 48,
"x": -24,
"y": -24
});
this.$svg.append(mouseOverRect);
this.$svg.append(this.$circle);
this.$svg.attr("id", "node" + this.getId());
this.$hiddenParentLine.attr({
"class": "hidden-link",
"x1": 0,
"y1": 0
});
this.$hiddenChildLine.attr({
"class": "hidden-link",
"x1": 0,
"y1": 0
});
}
}
/**
* Global variable used to assign each node an unique id
*
* @private
* @static
*/
VisualNode.id = 0;
VisualNode.prototype.getSVG = function() {
return this.$svg;
};
/**
* Gets this VisualNode's globally unique ID
*
* @returns {Number} The unique ID
*/
VisualNode.prototype.getId = function() {
return this.id;
};
/**
* Gets the underlying {@link ModelNode} that this VisualNode is a visualization
* of
*
* @returns {ModelNode} The underlying node
*/
VisualNode.prototype.getNode = function() {
return this.node;
};
/**
* Gets the x coordinate of the center of the VisualNode.
*
* @returns {Number} The x-coordinate
*/
VisualNode.prototype.getX = function() {
return this.x;
};
/**
* Sets the x coordinate of the center of the drawing of VisualNode.
*
* @param {Number} newX The new x-coordinate
*/
VisualNode.prototype.setX = function(newX) {
var translateX = this.isStart() ? newX - (Global.HOST_SIZE / 2) : newX;
this.x = newX;
this.$svg.attr("transform", "translate(" + translateX + "," + this.getY() + ")");
};
/**
* Gets the y coordinate of the center of the VisualNode.
*
* @returns {Number} The y-coordinate
*
*/
VisualNode.prototype.getY = function() {
return this.y;
};
/**
* Sets the y coordinate of the center of the VisualNode.
*
* @param {Number} newY The new y-coordinate
*/
VisualNode.prototype.setY = function(newY) {
this.y = newY;
this.$svg.attr("transform", "translate(" + this.getX() + "," + newY + ")");
};
/**
* Gets the radius of the VisualNode
*
* @returns {Number} The radius
*/
VisualNode.prototype.getRadius = function() {
return this.radius;
};
/**
* Sets the radius of the VisualNode
*
* @param {Number} newRadius The new radius
*/
VisualNode.prototype.setRadius = function(newRadius) {
this.radius = newRadius;
this.$circle.attr("r", newRadius);
if(this.hasHiddenParent()) {
this.$hiddenParentLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + newRadius,
"y2": -(Global.HIDDEN_EDGE_LENGTH + newRadius)
});
}
if(this.hasHiddenChild()) {
this.$hiddenChildLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + newRadius,
"y2": Global.HIDDEN_EDGE_LENGTH + newRadius
});
}
};
/**
* Gets the polygon points of a unique VisualNode
*
* @returns {Array.<Number>} The polygon points
*/
VisualNode.prototype.getPoints = function() {
return this.points;
};
/**
* Sets the polygon points of a unique VisualNode
*
* @param {Number} x, the new non-zero x coordinates (see updateNodeShape method)
* @param {Number} y, the new non-zero y coordinates
*/
VisualNode.prototype.setPoints = function(x,y) {
this.updateNodeShape(x,y);
if(this.hasHiddenParent()) {
this.$hiddenParentLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + x,
"y2": -(Global.HIDDEN_EDGE_LENGTH + y)
});
}
if(this.hasHiddenChild()) {
this.$hiddenChildLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + x,
"y2": Global.HIDDEN_EDGE_LENGTH + y
});
}
};
/**
* Gets the fill color of the VisualNode.
*
* @returns {String} The fill color
*/
VisualNode.prototype.getFillColor = function() {
return this.fillColor;
};
/**
* Sets the fill color of the VisualNode.
*
* @param {String} newFillColor The new fill color. The color must be a string
* that parses to a valid SVG color as defined in
* http://www.w3.org/TR/SVG/types.html#WSP
* @param {Boolean} isTemporary When true, this VisualNode will still produce
* its previous colour when calling getFillColor, and calling
* resetFillColor will set it.
* When false, the record of the previous colour is lost (default).
*/
VisualNode.prototype.setFillColor = function(newFillColor, isTemporary=false) {
if (!isTemporary) {
this.fillColor = newFillColor;
}
this.$circle.attr("fill", newFillColor);
this.$rect.attr("fill", newFillColor);
this.$diamond.attr("fill", newFillColor);
};
/**
* Sets the fillcolour to what was previously recorded prior to last
* non-temporary call to setFillColor.
*
*/
VisualNode.prototype.resetFillColor = function() {
this.setFillColor(this.fillColor);
};
/**
* Sets the hostlabel colour of this node.
* Precondition: this VisualNode represents a host node
*
*
* @param {String} newTextColor The new text color. The color must be a string
* that parses to a valid SVG color as defined in
* http://www.w3.org/TR/SVG/types.html#WSP
*/
VisualNode.prototype.setHostLabelColor = function(newTextColor) {
const $text = this.$rect.siblings("text");
$text.attr("fill", newTextColor);
};
/**
* Gets the stroke color of the VisualNode.
*
* @returns {String} The fill color
*/
VisualNode.prototype.getStrokeColor = function() {
return this.strokeColor;
};
/**
* Sets the stroke color of the VisualNode.
*
* @param {String} newStrokeColor The new stroke color. The color must be a
* string that parses to a valid SVG color as defined in
* http://www.w3.org/TR/SVG/types.html#WSP
*/
VisualNode.prototype.setStrokeColor = function(newStrokeColor) {
this.strokeColor = newStrokeColor;
this.$circle.attr("stroke", newStrokeColor);
this.$rect.attr("stroke", newStrokeColor);
this.$diamond.attr("stroke", newStrokeColor);
};
/**
* Sets the stroke width in px
*
* @param {Number} newStrokeWidth The new stroke width in units of px
*/
VisualNode.prototype.setStrokeWidth = function(newStrokeWidth) {
this.strokeWidth = newStrokeWidth;
this.$circle.attr("stroke-width", newStrokeWidth + "px");
this.$rect.attr("stroke-width", newStrokeWidth + "px");
this.$diamond.attr("stroke-wdith", newStrokeWidth + "px");
};
/**
* Gets the stroke width in units of px
*
* @returns {Number} The stroke width in units of px
*/
VisualNode.prototype.getStrokeWidth = function() {
return this.strokeWidth;
};
/**
* Gets the opacity of the node
*
* @returns {Number} The opacity
*/
VisualNode.prototype.getOpacity = function() {
return this.opacity;
};
/**
* Sets the opacity
*
* @param {Number} opacity The opacity
*/
VisualNode.prototype.setOpacity = function(opacity) {
this.opacity = opacity;
this.$circle.attr("opacity", opacity);
this.$diamond.attr("opacity", opacity);
};
/**
* Gets the VisualNode's label text. The label text is displayed inside the
* VisualNode itself
*
* @returns {String} The label text
*/
VisualNode.prototype.getLabel = function() {
return this.label;
};
/**
* Sets the VisualNode's label text. The label text is displayed inside the
* VisualNode itself
*
* @param {String} newLabel The new label text
*/
VisualNode.prototype.setLabel = function(newLabel) {
newLabel += "";
if(this.label.trim() == "" && newLabel.trim() != "") {
this.$svg.append(this.$text);
}
if(this.label.trim() != "" && newLabel.trim() == "") {
this.$text.remove();
}
this.label = newLabel;
this.$text.text(newLabel);
};
/**
* Gets the texual description of the VisualNode.
*
* @returns {String} The text
*/
VisualNode.prototype.getText = function() {
if (this.isStart())
return this.getHost();
else if (!this.isCollapsed())
return this.node.getFirstLogEvent().getText();
else
return this.node.getLogEvents().length + " collapsed events";
};
/**
* Gets the VisualNode's host. This will be the same as the host of the
* underlying node.
*
* @returns {String} The host
*/
VisualNode.prototype.getHost = function() {
return this.node.getHost();
};
/**
* Gets the line number in the original log text associated with this VisualNode
*
* @returns {Number} The line number
*/
VisualNode.prototype.getLineNumber = function() {
return this.node.getFirstLogEvent().getLineNumber();
};
/**
* Determines if this VisualNode is the special starting node of its host. The
* start node will be drawn differently from non-start nodes.
*
* @returns {Boolean} True if this is a start VisualNode
*/
VisualNode.prototype.isStart = function() {
return this.node.isHead();
};
/**
* Determines if this VisualNode is the last node of its host.
* The last node will have an event handler on it for grey-ing out its host.
*
* @returns {Boolean} True if this is the last VisualNode
*/
VisualNode.prototype.isLast = function() {
const nextNode = this.node.getNext();
return nextNode === null || nextNode.isTail();
};
/**
* Determines if this should be drawn with an edge to a hidden parent.
*
* @returns {Boolean} True if edge should be drawn
*/
VisualNode.prototype.hasHiddenParent = function() {
return this.hasHiddenParentInner;
};
/**
* Sets if this should be drawn with an edge to a hidden parent.
*
* @param {Boolean} val True if edge should be drawn
*/
VisualNode.prototype.setHasHiddenParent = function(val) {
if(this.hasHiddenParentInner && !val) {
this.$hiddenParentLine.remove();
}
else if(!this.hasHiddenParentInner && val) {
this.$hiddenParentLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + this.getRadius(),
"y2": -(Global.HIDDEN_EDGE_LENGTH + this.getRadius())
});
this.$svg.append(this.$hiddenParentLine);
}
this.hasHiddenParentInner = val;
};
/**
* Determines if this should be drawn with an edge to a hidden child.
*
* @returns {Boolean} True if edge should be drawn
*/
VisualNode.prototype.hasHiddenChild = function() {
return this.hasHiddenChildInner;
};
/**
* Sets if this should be drawn with an edge to a hidden child.
*
* @param {Boolean} val True if edge should be drawn
*/
VisualNode.prototype.setHasHiddenChild = function(val) {
if(this.hasHiddenChildInner && !val) {
this.$hiddenChildLine.remove();
}
else if(!this.hasHiddenChildInner && val) {
this.$hiddenChildLine.attr({
"x2": Global.HIDDEN_EDGE_LENGTH + this.getRadius(),
"y2": Global.HIDDEN_EDGE_LENGTH + this.getRadius()
});
this.$svg.append(this.$hiddenChildLine);
}
this.hasHiddenChildInner = val;
};
/**
* Determines if this VisualNode is a collapsed set of single nodes.
*
* @returns {Boolean} True if this is a collapsed node.
*/
VisualNode.prototype.isCollapsed = function() {
return this.node.getLogEvents().length > 1;
};
/**
* Determines if this VisualNode is highlighted.
*
* @returns {Boolean} True if this node is highlighted
*/
VisualNode.prototype.isHighlighted = function() {
return this._isHighlighted;
};
/**
* Sets if this VisualNode is highlighted.
*
* @param {Boolean} val True if this node is highlighted
*/
VisualNode.prototype.setHighlight = function(val) {
if(this._isHighlighted && !val) {
this.$highlightRect.remove();
}
else {
this.$highlightRect.attr({
"width": "15px",
"height": "15px",
"x": "5px",
"y": "5px"
});
this.$svg.append(this.$highlightRect);
}
this._isHighlighted = val;
};
/**
* Returns whether the node is selected
*
* @returns {Boolean} True if the node is selected
*/
VisualNode.prototype.isSelected = function() {
return this._isSelected;
};
/**
* Sets if the node is selected
*
* @param {Boolean} val True if the node is selected
*/
VisualNode.prototype.setSelected = function(val) {
this._isSelected = val;
};
/**
* Update the shape of the unique head node to a diamond shape
* (unique meaning it only shows up in one of the two active views)
*/
VisualNode.prototype.drawHostAsRhombus = function() {
this.points = [12,0,22,12,12,24,2,12];
this.$diamond.attr({
"width": Global.HOST_SIZE,
"height": Global.HOST_SIZE,
"points": this.points.join()
});
this.$rect.remove();
this.$svg.append(this.$diamond);
};
/**
* Update the shape of the unique non-start node to a diamond shape
* (unique meaning it only shows up in one of the two active views)
*/
VisualNode.prototype.drawEventAsRhombus = function(x,y) {
this.points = [0,-y,x,0,0,y,-x,0];
this.$diamond.attr("points", this.points.join());
this.$circle.remove();
this.$svg.append(this.$diamond);
this.$svg.append(this.$text);
};