Source: listenerObserverPGM.js

/*=============================================================*/
/*=================== ListenerObserverPGM =====================*/
/*=============================================================*/

"use strict";

/**
 * The ListenerObserverPGM is a wrapper that combines both listenerBeliefPGM and ListenerPGM.
 */
class ListenerObserverPGM {

    /**
     *  Create a ListenerObserverPGM with uniform weight distribution by default.
     * @param {string} divID - The id of the html tag that contains this pgm, it is of the form '#id_name'.
     * @param {object} listenerBeliefConfig - Listener's belief pgm configuration.
     * @param {object} listenerConfig - ListenerPGM's configuration.
     * @param {object} adjMatConfig - Weighted adjacency matrix configuration.
     * @param {array} cMat - The label cluster matrix holds the labels. Ex of cluster mat [layer1_label_array, layer2_label_array, layer3_label_array] where each layer_label_array holds an array of labelrs in one layer.
     * @param {array} speakerLayerProbabilityDistribution - The probability distribution of the speaker layer nodes.
     * @param {boolean} changeNodeRadiusBasedOnDistribution - Change speaker layer nodes radius based on the probability distribution.
     */
    constructor(divID, listenerBeliefConfig, listenerConfig, adjMatConfig, cMat, speakerLayerProbabilityDistribution, changeNodeRadiusBasedOnDistribution) {

        this._divID = divID;
        // parepare two id html elemnts for both listener and listener's belief pgms
        let listenerBeliefID = divID + "ListenerBeliefPGM";
        let listenerID = divID + "ListenerPGM";

        let $listenerBelief = $("<div>", {
            id: listenerBeliefID.substring(1)
        });
        let $listener = $("<div>", {
            id: listenerID.substring(1)
        });

        $(this._divID).append($listenerBelief);
        $(this._divID).append($listener);

        // Creating ListenerBeliefPGM first
        this.listenerBelif = new ListenerBeliefPGM(listenerBeliefConfig, listenerBeliefID)
            .createCluster(cMat, speakerLayerProbabilityDistribution, changeNodeRadiusBasedOnDistribution)
            .createAdjacencyMatrix(adjMatConfig)
            .init();

        // Add image
        // this.listenerBelif.svg.append("svg:image")
        //     .attr('x', -9)
        //     .attr('y', -12)
        //     .attr('width', 200)
        //     .attr('height', 200)
        //     .attr("xlink:href", "img/1.png");

        // Then create ListenerPGM first based on the configuration
        // and bind the data to the graph for rendering
        let listenerClusterMatrix = [cMat[1], cMat[0]]; // mirror image of the belisef graph
        this.listener = new ListenerPGM(listenerConfig, listenerID)
            .createCluster(listenerClusterMatrix, [], true)
            .init();

        this.listener.bindToListenerBeliefPGM(this.listenerBelif)
    }


    /**
     * Set the adjacency edges for a vertex by id.
     * @param {number} vertexId - The id or the index of the vertex in the data array of the graphData.
     * @param {object} edges - The object contains the adjacency edges of a vertex and their weights.
       return this pgm to allow setEdgeWeights to be stacked.
     * @example
     * // Create three directed edges 0->3, 0->4, 0->5 with edge weiths 0.8, 0.1 and 0.1
     * listenerObserverPGM.setEdgeWeights(0, [{
            id: 3,
            weight: 0.8
        }, {
            id: 4,
            weight: 0.1
        }, {
            id: 5,
            weight: 0.1
        }]);
     * @return {object} This grpahicalModel object.
     */
    setEdgeWeights(vertexId, edges) {
        this.listenerBelif.setEdgeWeights(vertexId, edges);
        this.listener.resetEdgeWeightsToBeListenerBeliefPGMEdgeWeights();
        return this;
    }

}



/**
 * The graphical model represents the listener's belief.
 * @extends GraphicalModel
 */
class ListenerBeliefPGM extends GraphicalModel {

    /**
     * Create a defiend space for the listener's belief graphical model in the form of a dialogue box.
     * @param {object} graphConfiguration - A configuration object for configuring the properties of this _pgm, it can be obtained via Config.getPGMConfig().
     * @param {string} divID - The id of the html tag that contains this pgm, it is of the form '#id_name'.
     */
    constructor(graphConfiguration, divID) {

        super(graphConfiguration, divID);

        // Creates the dialgue box here
        this._createDialogueBox();
    }


    /**
     * This method creates the dialogue box for listener's belief to achieve thought bubble effect. The dialogue box is consisited of resized background rect from the listener's belief PGM and a small upsidedown triangle, which is used to give the feeling of a dialgoue bubble.
     * @private
     */
    _createDialogueBox() {

        let rectX = this.config.transform.width / 4,
            rectY = this.config.transform.height * 2 / 15,
            rectWidth = this.config.transform.width / 2,
            rectHeight = this.config.transform.height * 3 / 4,
            rectCornorRadius = this.config.transform.width / 15;
        this.rect
            .attr("class", "background")
            .attr("x", rectX)
            .attr("y", rectY)
            .attr("rx", rectCornorRadius)
            .attr("ry", rectCornorRadius)
            .attr("width", rectWidth)
            .attr("height", rectHeight)
            .style("fill", this.config.background.color)
            .style("pointer-events", "all")
            .on("click", d => {
                pgm._backgroundOnClick();
            });

        let offSet = -1,
            trianglePoint1 = [rectX + rectWidth / 3, rectY + rectHeight + offSet],
            trianglePoint2 = [rectX + rectWidth / 2, this.config.transform.height],
            trianglePoint3 = [rectX + rectWidth * 2 / 3, rectY + rectHeight + offSet];
        let trianglePath = trianglePoint1[0] + "," + trianglePoint1[1] + ", " + trianglePoint2[0] + "," + trianglePoint2[1] + ", " + trianglePoint3[0] + "," + trianglePoint3[1];
        this.svg.append("polygon") // attach a polygon
        .style("fill", this.config.background.color)
            .attr("points", trianglePath);
    }

    /**
     * Calculate the edge weights of the listener grpah based on the adjacency matrix.
     * @private
     * @return {array} normalizedWeight - The normalized weights in a 1D array in the form of Wij, ex, [W_sub(1,1), W_sub(1,2), W_sub(2,1), W_sub(2,2)].
     */
    calculateWeights() {

        let weight = [];

        let firstLayerLength = this.graphData.clusterMat[0].length;
        let lastLayerLength = this.graphData.clusterMat[this.graphData.clusterMat.length - 1].length;
        // Calculate the new edge weights based on adj matrix
        for (let i = 0; i < firstLayerLength; i++) {
            for (let j = 0; j < lastLayerLength; j++) {
                let M_ij = this.getWeightedAdjacencyMatrix().getCellWeight([i, j]);
                let Mij_summation_over_j = 0;
                for (let sigma_sub_j = 0; sigma_sub_j <= j; sigma_sub_j++) {
                    Mij_summation_over_j += this.getWeightedAdjacencyMatrix().getCellWeight([i, sigma_sub_j]);
                }
                // log([M_ij, Mij_summation_over_j])
                let W_ij = (M_ij === 0) ? 0 : M_ij / Mij_summation_over_j;
                weight.push(W_ij);
            }
        }

        log("Weight = " + weight);

        // Noralize the weights so that all node's edge weights sum up to 1

        let vertexWeightSumTemp = []; // each element is the weight sum for a vertex
        let weightIdx = 1;
        let tempWeightSumForVertex = 0;
        for (let i = 0; i < weight.length; i++) {
            tempWeightSumForVertex += weight[i];
            if (weightIdx % firstLayerLength == 0) {
                // push vertex sum "firstLayerLength" many times so its easier to normalize weight
                for (let j = 0; j < firstLayerLength; j++) {
                    vertexWeightSumTemp.push(tempWeightSumForVertex);
                }
                tempWeightSumForVertex = 0;
            }
            weightIdx++;
        }

        log("vertexWeightSumTemp = " + vertexWeightSumTemp);

        // Normalize
        let normalizedWeight = [];
        for (let i = 0; i < weight.length; i++) {
            normalizedWeight[i] = (vertexWeightSumTemp[i] === 0) ? 0 : weight[i] / vertexWeightSumTemp[i];
        }

        log("normalized weight = " + normalizedWeight);

        return normalizedWeight;
    }

    _cyclingSpeedControlButtonOnClick(ui) {
        super._cyclingSpeedControlButtonOnClick(ui);
        // updating listenerPGM's speed on button click as well
        this.listenerPGM.config.edge.timeInterval = ui.value;
        this.listenerPGM.config.autoPlay.timeIntervalBetweenCycle = ui.value;
    }

    /**
     * Update the adjacency matrix, After updating the matrix also updating the weight in ListenerPGM.
     * @private
     * @override
     */
    _updateChart() {
        super._updateChart();

        let updatedWeights = this.calculateWeights()
        this.listenerPGM.updateWeight(updatedWeights);
        this.listenerPGM.redraw();
    }


    /**
     * Stop autoPlay.
     * @private
     * @override
     */
    _stopAutoPlay() {
        super._stopAutoPlay();
        // When stop button is clicked, reset the listenerPGM edgeweights as well
        // this.listenerPGM.resetEdgeWeightsToBeListenerBeliefPGMEdgeWeights();
        // this.listenerPGM.redraw();
    }

    /**
     * When stop button is clicked, reset the listenerPGM edgeweights as well.
     * @private
     * @override
     */
    _startAutoPlay() {
        // Stop listenerPGM autoplay before calling super._startPlay() so that _clearVisitedPath() method won't destroy both listenerPGM's autoPlay and this graph's autoPlay
        if (this.listenerPGM.config.autoPlay.on) {
            this.listenerPGM._stopAutoPlay();
        }

        super._startAutoPlay();

        // reset the listenerPGM edge weights
        this.listenerPGM.resetEdgeWeightsToBeListenerBeliefPGMEdgeWeights();
        this.listenerPGM.redraw();
    }

    /**
     * Used to create an array of vertix data in graphData based on the label cluster matrix (cMat). The graph edge weights are set to be uniform by defaut.
     * @override
     * @param {array} cMat - The label cluster matrix holds the labels. Ex of cluster mat [layer1_label_array, layer2_label_array, layer3_label_array] where each layer_label_array holds an array of labelrs in one layer.
     * @param {array} probabilityDistribution - The array of probability given to each node in the speaker layer to be triggered. For uniform distribution, set probabilityDistribution = [].
     * @param {boolean} changeNodeRadiusBasedOnDistribution - Governs whether vertex radius are affected by its distribution.
     * @return {object} This graphicalModel object.
     * @example
     * graphicalModel.createCluster(clusterMat, speakerNodeProbabilityDistribution, true);
     */
    createCluster(cMat, probabilityDistribution, changeNodeRadiusBasedOnDistribution) {

        this.listenerClusterMatrix = [cMat[1], cMat[0]]; // mirror image of the belisef graph
        this.listenerBeliefClusterMatrix = cMat;

        if (cMat.length != 2)
            throw new Error("ListenerBeliefPGM.createCluster(): invalid cMat length. This graph only supports two layer graphs.");
        super.createCluster(this.listenerBeliefClusterMatrix, probabilityDistribution, changeNodeRadiusBasedOnDistribution);
        return this;
    }

    /**
     * Bind the listenerBeliefPGM to the ListenerPGM.
     * @param {object} listener - The listenerBeliefPGM.
     * @return {object} this listenerBeliefPGM object.
     */
    bindToListenerPGM(listener) {
        this.listenerPGM = listener;
        return this;
    }
}



/**
 * The graphical model represents the listener pgm.
 * @extends GraphicalModel
 */
class ListenerPGM extends GraphicalModel {

    /**
     * Create a defiend space for the listener graphical model.
     * @param {object} graphConfiguration - A configuration object for configuring the properties of this _pgm, it can be obtained via Config.getPGMConfig().
     * @param {string} divID - The id of the html tag that contains this pgm, it is of the form '#id_name'.
     */
    constructor(graphConfiguration, divID) {
        super(graphConfiguration, divID);
    }


    /**
     * Reset the adjacency matrix attached to this graphical model when the background is clicked. It is called in the on click listener function defined within this graphical model.
     * @private
     * @override
     */
    _backgroundOnClick() {
        // Prevent adjMatrix gets reset when click on background
        if (this._canClick && !this.listenerBeliefPGM.config.autoPlay.on) {
            super._backgroundOnClick();
        }
    }

    /**
     * Reset the listenerPGM's edge weights to be listener's belief edge weights.
     */
    resetEdgeWeightsToBeListenerBeliefPGMEdgeWeights() {
        // Set listenerPGM to be listenerBeliefPGM's mirror
        let weights = [];
        for (let i = 0; i < this.cMatDim[0]; i++) {
            for (let vertexIdx = 0; vertexIdx < this.listenerBeliefPGM.cMatDim[0]; vertexIdx++) {
                let listenerEdgeWeights = this.listenerBeliefPGM.graphData.data[vertexIdx].edgeWeights;
                weights.push(listenerEdgeWeights[i].weight);
            }
        }
        this.updateWeight(weights);
    }

    /**
     * Binds the listener and the listener's belief to each other and set listener's weight.
     */
    bindToListenerBeliefPGM(belief) {

        belief.bindToListenerPGM(this);
        this.listenerBeliefPGM = belief;

        let listenerPGM = this;
        // Redefine onClick when bind to listenerBeliefPGM to prevent adjMatrix gets reset when click on background
        this.onClick = d3.behavior.drag()
            .origin(d => d)
            .on("dragstart", function(d) {
                // Check if the clicked node is in the first layer
                // which are the num of nodes in first layer of clusterMat
                // Only allow user to click the node if autoplay is off
                if (!listenerPGM.listenerBeliefPGM.config.autoPlay.on) {
                    d3.event.sourceEvent.stopPropagation();
                    d3.select(this).classed("dragging", true);

                    // Option 1: Only draw visited path once
                    // listenerPGM._triggerSpeakerNode(this.id);

                    // Option 2: AutoPlay when speaker node is clicked
                    let speakerLayerLength = listenerPGM.graphData.clusterMat[0].length;
                    if (listenerPGM.config.autoPlay.on) {
                        listenerPGM._stopAutoPlay();
                    } else {
                        listenerPGM._startAutoPlay();
                    }
                }
            });
        this.resetEdgeWeightsToBeListenerBeliefPGMEdgeWeights();

        return this;
    }

    /**
     * Used to update the listenerPGM's edge weights.
     * @param {array} weight - The new weights as an 1D array.
     */
    updateWeight(weight) {

        let weightIdx = 0;
        for (let vertexIdx = 0; vertexIdx < this.cMatDim[0]; vertexIdx++) {
            let edgeWeights = [];
            for (let i = 0; i < this.cMatDim[1]; i++) {
                edgeWeights[i] = {
                    id: this.getVertexId([1, i]),
                    weight: weight[weightIdx]
                };
                weightIdx++;
            }
            this.setEdgeWeights(vertexIdx, edgeWeights);
        }
    }


}