/*=============================================================*/
/*=================== 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);
}
}
}