Source: visualization/view.js

/**
 * Constructs a View that draws the specified model
 * 
 * @class
 * 
 * A View is responsible for drawing a single VisualGraph.
 * 
 * @constructor
 * @param {ModelGraph} model
 * @param {HostPermutation} hostPermutation
 * @param {String} label
 */
function View(model, hostPermutation, label) {
    
    /** @private */
    this.$svg = $(document.createElementNS('http://www.w3.org/2000/svg', 'svg'));
    
    /** @private */
    this.$hostSVG = $(document.createElementNS('http://www.w3.org/2000/svg', 'svg'));
    
    /** @private */
    this.logTable = $("<td></td>");
    
    /** @private */
    this.hostPermutation = hostPermutation;

    /** @private */
    this.label = label;

    /** @private */
    this.initialModel = model;

    /** @private */
    this.layout = new SpaceTimeLayout(0, 56);

    /** @private */
    this.visualGraph = new VisualGraph(model, this.layout, hostPermutation);

    /** @private */
    this.transformer = new Transformer();
    
    /** @private */
    this.controller = null;
    
    /**
     * Cacheed mapping of hostnames to abbreviated hostnames
     * @private
     */
    this.abbreviatedHostnames = null; // {String} -> {String}

    /** 
    * Used to determine if a tailnode is scrolling out of view
    * @private
    */
    this.tailNodes = [];

    /**
    * A mapping of hostnode names to their visual nodes
    * @private
    */
    this.hostNodes = new Map();
}

/**
 * Gets the transformer associated with this view. In other words, the
 * transformer configured for and responsible for transforming the
 * {@link VisualGraph} that this view draws.
 * 
 * @returns {Transformer} The transformer associated with this view
 */
View.prototype.getTransformer = function() {
    return this.transformer;
};

View.prototype.getSVG = function() {
    return this.$svg;
};

View.prototype.getHostSVG = function() {
    return this.$hostSVG;
};

View.prototype.getLogTable = function() {
    return this.logTable;
};

/**
 * Gets the hosts as an array
 * 
 * @returns {Array<String>} The hosts
 */
View.prototype.getHosts = function() {
    return this.initialModel.getHosts();
};

/**
 * Gets the model
 * 
 * @returns {Graph} The model
 */
View.prototype.getModel = function() {
    return this.initialModel;
};

/**
 * Gets the label
 * 
 * @returns {Graph} The label
 */
View.prototype.getLabel = function() {
    return this.label;
};

/**
 * Gets the current visual model
 * 
 * @returns {VisualGraph} The current model
 */
View.prototype.getVisualModel = function() {
    return this.visualGraph;
};

/**
 * Sets the width of this view
 * 
 * @param {Number} newWidth The new width
 */
View.prototype.setWidth = function(newWidth) {
    this.layout.setWidth(newWidth);
};

View.prototype.setLogTableWidth = function(newWidth) {
    this.logTable.width(newWidth + "pt");
};

/**
 * Returns whether this modelGraph has the given host
 * 
 * @returns {Boolean} True if the graph has this particular host
 */
View.prototype.hasHost = function(host) {
    return this.initialModel.hasHost(host);
};

/**
 * Returns whether this modelGraph has structures matching the current query
 *
 * @returns {Boolean} True if this modelGraph has elements matching the current search
 */
View.prototype.hasQueryMatch = function() {
    var hmt = this.getTransformer().getHighlightMotifTransformation();
    if (hmt != null) { 
        hmt.findMotifs(this.initialModel);
        return hmt.getHighlighted().getMotifs().length > 0;
    } else {
        return false;
    }
}

/**
 * Clears the current visualization and re-draws the current model.
 */
View.prototype.draw = function(viewPosition) {

    this.model = this.initialModel.clone();
    this.visualGraph = new VisualGraph(this.model, this.layout, this.hostPermutation);
    this.transformer.transform(this.visualGraph);

    // Update the VisualGraph
    this.visualGraph.update();

    // Define locally so that we can use in lambdas below
    var view = this;

    this.$svg.children("*").remove();

    this.$svg.attr({
        "height": this.visualGraph.getHeight(),
        "width": this.visualGraph.getWidth()
    });
    
    var hackyFixRect = Util.svgElement("rect");
    hackyFixRect.attr({
        "height": this.visualGraph.getHeight() + "px",
        "width": this.visualGraph.getWidth() + "px",
        "opacity": 0,
        "stroke-width": "0px",
        "z-index": -121
    });
    this.$svg.append(hackyFixRect);

    drawLinks();
    drawNodes();
    drawHosts();
    drawLogLines();

    // Hide line highlight
    $(".highlight").hide();

    function drawLinks() {
        view.visualGraph.getVisualEdges().forEach(function(visualEdge) {
            view.$svg.append(visualEdge.getSVG());
        });
    }

    function drawNodes() {
        var nodes = view.visualGraph.getNonStartVisualNodes();
        var arr = [];
        nodes.forEach(function(visualNode) {
            var svg = visualNode.getSVG();
            view.$svg.append(svg);
            arr.push(svg[0]);
            if (visualNode.isLast()) {
                view.tailNodes.push(visualNode);
            }
        });

        // Bind the nodes
        view.controller.bindNodes(d3.selectAll(arr).data(nodes));
    }

    function drawHosts() {
        
        view.$hostSVG.children("*").remove();
        
        view.$hostSVG.attr({
            "width": view.visualGraph.getWidth(),
            "height": Global.HOST_SIZE,
            "class": view.id
        });

        if (viewPosition == "R") {
            view.$hostSVG.css("margin-left", ".15em");
        }
        
        else {
            view.$hostSVG.css("margin-left", "0em");
        }
        var startNodes = view.visualGraph.getStartVisualNodes();
        var arr = [];
        startNodes.forEach(function(visualNode) {
            view.hostNodes.set(visualNode.getHost(), visualNode);
            var svg = visualNode.getSVG();
            view.$hostSVG.append(svg);
            arr.push(svg[0]);
        });

        // Bind the hosts
        view.controller.bindHosts(d3.selectAll(arr).data(startNodes));

        drawHostLabels(arr);
    }

    function drawHostLabels(g_hosts) {
        view.$hostSVG.attr("overflow", "visible");

        var hosts = d3.selectAll(g_hosts);
        var x_offset = Global.HOST_SIZE / 3;
        var hostLabels = hosts.append("text")
            .text(function(node) {
                const label = view.getAbbreviatedHostname(node.getHost());
                return label;
            })
            //.attr("text-anchor", "middle")
            .attr("transform", "rotate(-45)")
            .attr("x", x_offset)
            .attr("y", "1em")
            .attr("font-size", "x-small");

        if (!view.hasAbbreviatedHostnames()) {
            setTimeout(function() {
                // Must abbreviate after timeout so that the text elements will have
                // been drawn. Otherwise they will have no computed text length.
                abbreviateD3Texts(hostLabels);
            });
        }
    }

    function abbreviateD3Texts(d3Texts) {
        const textsToFitter = getD3FitterMap(d3Texts, Global.HOST_LABEL_WIDTH);
        const textsToSetter = getD3SetterMap(d3Texts);

        // Must come after after creating the textsToXXXMaps, since it mutates
        // the d3Texts
        const abbrevs = Abbreviation.generateFromStrings(textsToFitter);

        for (let abbrev of abbrevs) {
            const hostname = abbrev.getOriginalString();
            view.setAbbreviatedHostname(hostname, abbrev.getEllipsesString());

            let setText = textsToSetter.get(hostname);
            setText(abbrev);
        }

        function getD3FitterMap(d3Texts, svgWidth) {
            const textsToFitter = new Map();
            d3Texts.each(function() {
                const d3Text = d3.select(this);
                const self = this;
                textsToFitter.set(d3Text.text(),
                    function (str) {
                        d3Text.text(str);
                        return self.getComputedTextLength() < svgWidth;
                    });
            });
            return textsToFitter;
        }
        
        function getD3SetterMap(d3Texts) {
            const textsToSetter = new Map();
            d3Texts.each(function() {
                const d3Text = d3.select(this);
                const setAbbrevText = makeAbbrevTextSetter(this);
                textsToSetter.set(d3Text.text(), setAbbrevText);
                    
            });
            return textsToSetter;
        }

        function makeAbbrevTextSetter(svgText) {
            const d3Text = d3.select(svgText);
            return function(abbrev) {
                d3Text.text(abbrev.getEllipsesString());
            };
        }
    }

    function drawLogLines() {
        view.logTable.empty();

        var lines = {};
        var visualNodes = view.getVisualModel().getVisualNodes();
        for (var i in visualNodes) {
            var node = visualNodes[i];
            var y = node.getY();
            if (lines[y] === undefined)
                lines[y] = [ node ];
            // nodes with the same y coordinate saved in lines[y]
            else
                lines[y].push(node);
        }

        delete lines[0];

        var $div = $("<div></div>");
        $div.addClass("logLabel" + viewPosition);
        $div.text(view.getLabel());
        view.logTable.append($div);

        for (var y in lines) {
            var overflow = null;
            var vn = lines[y];
            // shift the log lines down by adding 20px or about 1.5em to the y coordinate
            var top = (Number(y) + 20).toString();
            var startMargin = (1 - Math.min(vn.length, 3)) / 2;

            if (vn.length > 3)
                overflow = vn.splice(2, vn.length);

            for (var i in vn) {
                var text = vn[i].getText();
                var $div = $("<div></div>", {
                    "id": "line" + vn[i].getId()
                }).data({
                    "id": vn[i].getId()
                }).addClass("line").css({
                    "top": top + "px",
                    "margin-top": startMargin + "em",
                    "color": vn[i].getFillColor(),
                    "opacity": vn[i].getOpacity()
                }).text(text);
                view.logTable.append($div);
                startMargin++;
            }

            if (overflow != null) {
                var $div = $("<div></div>").addClass("line more").css({
                    "top": top + "px",
                    "margin-top": (startMargin * 10) + "pt",
                    "color": "#ddd"
                }).text("+ " + overflow.length + " more");

                for (var o in overflow) {
                    var text = overflow[o].getText();
                    $div.append($("<div></div>", {
                        "id": "line" + overflow[o].getId()
                    }).data({
                        "id": overflow[o].getId()
                    }).addClass("line").css({
                        "margin-top": o + "em",
                        "color": overflow[o].getFillColor(),
                        "opacity": vn[i].getOpacity()
                    }).text(text));
                    startMargin++;
                }

                view.logTable.append($div);
            }
        }

    }
}
/**
 * Returns true if the abbrviated hostname strings have been cached.
 * @return {boolean} 
 */
View.prototype.hasAbbreviatedHostnames = function() {
    return this.abbreviatedHostnames !== null;
};

/**
 * Gets the abbreiviated hostname string associated with given hostname
 * string. If no abbreviation is recorded, then returns original string.
 * @param {string} hostname
 * @return {string} abbreviated hostname
 */
View.prototype.getAbbreviatedHostname = function(hostname) {
    if (this.hasAbbreviatedHostnames() &&
        this.abbreviatedHostnames.has(hostname)) {
        return this.abbreviatedHostnames.get(hostname);
    } else {
        return hostname;
    }
};

/**
 * Caches the abbreviated hostname, creating the cache if necessary
 * @param {string} hostname Complete hostname
 * @param {string} abbrev Abbreviated, ellipsified hostname
 */
View.prototype.setAbbreviatedHostname = function(hostname, abbrev) {
    if (!this.hasAbbreviatedHostnames()) {
        this.abbreviatedHostnames = new Map();
    }
    this.abbreviatedHostnames.set(hostname, abbrev);
}

/**
 * If isScrolledPast is true, changes colour of visualNode's host to grey.
 * If it is false, changes host node back to the original colour
 * @param {VisualNode} 
 * @param {Boolean}
 */
View.prototype.setGreyHost = function(visualNode, isScrolledPast) {
    const view = this;
    const visualHostNode = this.hostNodes.get(visualNode.getHost());
    if (isScrolledPast) {
        // set to grey
        const isTemporary = true;
        visualHostNode.setFillColor("white", isTemporary);
        visualHostNode.setStrokeColor("lightgrey");
        visualHostNode.setHostLabelColor("grey");
        visualHostNode.setStrokeWidth(1);
    } else {
        // reset to original colour
        visualHostNode.resetFillColor();
        visualHostNode.setHostLabelColor("black");
        visualHostNode.setStrokeColor(Global.NODE_STROKE_COLOR);
        visualHostNode.setStrokeWidth(Global.NODE_STROKE_WIDTH);
    }
}

/**
 * Return the tail nodes of each host in this view's graph
 * @return {VisualNode[]}
 */
View.prototype.getTailNodes = function() {
    return this.tailNodes;
};