/**
* Handles all GUI functionality.
* @module GUI
*/
var guiController = {};
var nodeDescriptionAttributes = [];
var gui = new dat.GUI();
var detailLODEnabled = false;
var sharedEdgeValuesFolder;
var sharedEdgeAttributesPrev = [];
var sharedEdgeAttributesPrevGUI = [];
var searchResultsFolder;
var searchResultIdsPrev = [];
var searchResultIdsPrevGUI = [];
var edgeSets = ['cast', 'keywords', 'genres'];
var edgeSetsLabelToAttributeMap = {};
var edgeSetName;
var edgeSet;
var wordCloudAttributes = ['cast', 'keywords', 'genres'];
var wordCloudLabelToAttributeMap = {};
var wordCloudAttribute;
var selectedClusterFeatures = [];
var clusterFeaturesControl = [];
var nodeNumericalAttributes = [];
var nodeSortingLabelToAttributeMap = {};
var nodesSortingAttribute;
var minEdgeWeightControl;
var maxEdgeWeightControl;
var selectedEdgeFolder;
var selectedNodeFolder;
var clusterSettingsFolder;
var displaySettingsFolder;
var searchFolder;
var detailLODButton;
var lowDetailLODButton;
var minNodesPerCluster = 10;
var defaultNodesPerCluster = 25;
var maxNodesPerCluster = 40;
var emptyFunction = function () { };
var loadingLabel = document.getElementById('loadingLabel');
/**
* initializes the entire GUI
*/
function initGUI() {
initGUINodeAttributes();
initGUIControllerAttributes();
initGUIControls();
showLowDetailLODControls();
}
/**
* prevent the loading label from being rendered
*/
function hideLoadingLabel() {
loadingLabel.style.visibility = "hidden";
}
/**
* render the loading label
*/
function showLoadingLabel() {
loadingLabel.style.visibility = "visible";
}
/**
* automatically detect the attributes of the visualized nodes (numerical ones and strings)
*/
function initGUINodeAttributes() {
// create lookupmaps for variable names
edgeSets.forEach(edgeSet => edgeSetsLabelToAttributeMap[attributeNameToLabelName(edgeSet)] = edgeSet);
wordCloudAttributes.forEach(attribute => wordCloudLabelToAttributeMap[attributeNameToLabelName(attribute)] = attribute)
// read description and sorting attributes automatically
nodeNumericalAttributes = [];
nodeDescriptionAttributes = [];
Object.keys(nodes[0]).forEach(function (key, index) {
if (!isNaN(nodes[0][key]) && key != 'id') {
let labelName = attributeNameToLabelName(key);
nodeSortingLabelToAttributeMap[labelName] = key;
nodeNumericalAttributes.push(labelName);
nodeDescriptionAttributes.push(key);
}
else if (typeof nodes[0][key] == 'string') {
nodeDescriptionAttributes.push(key);
}
});
}
/**
* initializes all variables controlled by the GUI
*/
function initGUIControllerAttributes() {
// clustering
guiController.clusters = parseInt(nodes.length / defaultNodesPerCluster);
guiController.clusterFeatures = {};
nodeNumericalAttributes.forEach((nodeNumericalAttribute, index) => guiController.clusterFeatures[nodeSortingLabelToAttributeMap[nodeNumericalAttribute]] = ((index == 3) ? true : false));
selectedClusterFeatures = [nodeSortingLabelToAttributeMap[nodeNumericalAttributes[3]]];
guiController.newClustering = function () {
console.log("adopt new cluster settings");
console.log("new number of clusters:" + guiController.clusters)
console.log("new cluster features:" + selectedClusterFeatures)
showLoadingLabel();
getSupernodesFromServer(setupNewLowDetailLODAndRender);
};
// edge set
guiController.edgeSet = Object.keys(edgeSetsLabelToAttributeMap)[0];
edgeSetName = edgeSetsLabelToAttributeMap[guiController.edgeSet];
// word cloud
guiController.wordCloud = Object.keys(wordCloudLabelToAttributeMap)[1];
wordCloudAttribute = wordCloudLabelToAttributeMap[guiController.wordCloud];
// weight filtering
guiController.minEdgeWeight = 1;
guiController.maxEdgeWeight = 10;
// node sorting
guiController.nodeSorting = nodeNumericalAttributes[0];
nodesSortingAttribute = nodeSortingLabelToAttributeMap[guiController.nodeSorting];
// node searching
guiController.searchTerm = '-';
// node description
nodeDescriptionAttributes.forEach(nodeDescriptionAttribute => guiController[nodeDescriptionAttribute] = "-");
// edge description
guiController.node1 = '-';
guiController.node2 = '-';
guiController.weight = '-';
// LOD switching
guiController.detailLOD = function () {
if (clickedSupernodes.length == 0)
console.log('Please select a supernode first to see its details.');
else {
detailLODEnabled = true;
supernodes = clickedSupernodes;
showLoadingLabel();
resetSelectedNode();
resetSelectedEdge();
guiUpdateSearchResults();
showDetailLODControls();
getEdgesOfSelectedSupernodesFromServer(setupNewDetailLODAndRender);
}
};
guiController.lowDetailLOD = function () {
detailLODEnabled = false;
showLoadingLabel();
guiUpdateSearchResults();
showLowDetailLODControls();
setupLowDetailLODAndRender();
};
}
/**
* initializes all GUI controls, buttons, labels, etc.
*/
function initGUIControls() {
initDisplaySettingsControls();
initClusterSettingsControls();
initNodeSearchControls();
initSelectedNodeControls();
initSelectedEdgeControls();
}
/**
* initializes all edge controls
*/
function initSelectedEdgeControls() {
// SELECTED EDGE
selectedEdgeFolder = gui.addFolder('Selected Edge');
selectedEdgeFolder.open();
addLabelToGUI(selectedEdgeFolder, 'node1', 'Node 1');
addLabelToGUI(selectedEdgeFolder, 'node2', 'Node 2');
addLabelToGUI(selectedEdgeFolder, 'weight', 'Weight');
sharedEdgeValuesFolder = selectedEdgeFolder.addFolder('Shared Values');
sharedEdgeValuesFolder.open();
}
/**
* initializes the node attributes display
*/
function initSelectedNodeControls() {
// SELECTED NODE
selectedNodeFolder = gui.addFolder('Selected Node');
selectedNodeFolder.open();
nodeDescriptionAttributes.forEach(nodeDescriptionAttribute => {
addLabelToGUI(selectedNodeFolder, nodeDescriptionAttribute, attributeNameToLabelName(nodeDescriptionAttribute));
});
}
/**
* initializes the node search control
*/
function initNodeSearchControls() {
// NODE SEARCH
searchFolder = gui.addFolder('Search Node');
let searchTermControl = searchFolder.add(guiController, 'searchTerm').name("Search Term");;
searchTermControl.onFinishChange(guiUpdateSearchResults);
searchResultsFolder = searchFolder.addFolder('Found Nodes');
searchResultsFolder.open();
}
/**
* creates the GUI controls for the display settings
*/
function initDisplaySettingsControls() {
// DISPLAY SETTINGS
displaySettingsFolder = gui.addFolder('Display Settings');
// min edge weight
minEdgeWeightControl = displaySettingsFolder.add(guiController, 'minEdgeWeight').name("Min Weight").min(1).max(10).step(1).listen();
minEdgeWeightControl.onFinishChange(function (value) {
console.log("min edge weight set to: " + guiController.minEdgeWeight);
setupDetailLODAndWordCloudsAndRender();
})
// max edge weight
maxEdgeWeightControl = displaySettingsFolder.add(guiController, 'maxEdgeWeight').name("Max Weight").min(1).max(10).step(1).listen();
maxEdgeWeightControl.onFinishChange(function (value) {
console.log("max edge weight set to: " + guiController.maxEdgeWeight);
setupDetailLODAndWordCloudsAndRender();
})
// node sorting attribute
let sortingControl = displaySettingsFolder.add(guiController, 'nodeSorting', nodeNumericalAttributes).name("Node Sorting");
sortingControl.onFinishChange(function (value) {
nodesSortingAttribute = nodeSortingLabelToAttributeMap[guiController.nodeSorting];
console.log("nodes sorted according to attribute: " + nodesSortingAttribute);
setupDetailLODAndWordCloudsAndRender();
});
// word cloud
let wordCloudAttributeControl = displaySettingsFolder.add(guiController, 'wordCloud', Object.keys(wordCloudLabelToAttributeMap)).name("Word Cloud")
wordCloudAttributeControl.onFinishChange(function (value) {
wordCloudAttribute = wordCloudLabelToAttributeMap[guiController.wordCloud];
console.log("word clouds according to attribute: " + wordCloudAttribute);
generateWordCloudsOnServer(createWordCloudRenderObjects);
});
}
/**
* creates the GUI controls for the clustering settings
*/
function initClusterSettingsControls() {
// CLUSTER SETTINGS
clusterSettingsFolder = gui.addFolder('Cluster Settings');
// edge set
let edgeSetControl = clusterSettingsFolder.add(guiController, 'edgeSet', Object.keys(edgeSetsLabelToAttributeMap)).name("Edge Set");
edgeSetControl.onFinishChange(function (value) {
edgeSetName = edgeSetsLabelToAttributeMap[guiController.edgeSet];
console.log("selected edge set: " + edgeSetName);
setupNewLowDetailLODAndRender();
});
let maxClusters = parseInt(nodes.length / minNodesPerCluster);
let minClusters = parseInt(nodes.length / maxNodesPerCluster);
clusterSettingsFolder.add(guiController, 'clusters').name("Clusters").min(minClusters).max(maxClusters).step(1);
//cluster feature control
for (var feature in guiController.clusterFeatures) {
clusterFeaturesControl.push(clusterSettingsFolder.add(guiController.clusterFeatures, feature).name(attributeNameToLabelName(feature))
.listen()
.onChange(
function (value) {
selectedClusterFeatures = [];
count = 0;
for (var feature in guiController.clusterFeatures) {
control = clusterFeaturesControl[count];
guiController.clusterFeatures[control.property] = control.getValue();
if (guiController.clusterFeatures[control.property]) {
selectedClusterFeatures.push(control.property);
}
count++;
}
}
));
}
clusterSettingsFolder.add(guiController, 'newClustering').name('Do New Clustering');
}
/**
* switches to the GUI controls of the lowDetailLOD view
*/
function showLowDetailLODControls() {
selectedEdgeFolder.hide();
selectedNodeFolder.hide();
displaySettingsFolder.hide();
clusterSettingsFolder.show();
if (lowDetailLODButton) {
gui.remove(lowDetailLODButton);
lowDetailLODButton = undefined;
}
if (!detailLODButton)
detailLODButton = gui.add(guiController, 'detailLOD').name('Show Details Of Selection');
}
/**
* switches to the GUI controls of the detailLOD view
*/
function showDetailLODControls() {
selectedEdgeFolder.show();
selectedNodeFolder.show();
displaySettingsFolder.show();
clusterSettingsFolder.hide();
if (detailLODButton) {
gui.remove(detailLODButton);
detailLODButton = undefined;
}
if (!lowDetailLODButton)
lowDetailLODButton = gui.add(guiController, 'lowDetailLOD').name('Show Supernode Overview');
}
/**
* updates the limits which can be set for the weight sliders
* @param {int} min new min value that can be set by the slider
* @param {int} max new max value that can be set by the slider
* @param {boolean} updateValues if true then the current values for the sliders are updated to the new limits
*/
function updateGUIEdgeWeightLimits(min, max, updateValues) {
minEdgeWeightLimit = min;
maxEdgeWeightLimit = max;
minEdgeWeightControl.min(minEdgeWeightLimit).max(maxEdgeWeightLimit);
maxEdgeWeightControl.min(minEdgeWeightLimit).max(maxEdgeWeightLimit);
if (updateValues) {
guiController.minEdgeWeight = minEdgeWeightLimit;
guiController.maxEdgeWeight = maxEdgeWeightLimit;
}
}
/**
* converts a property name of a json object (e.g. stringControl or string_control) to a label (i.e. String Control)
* @param {string} attributeName a valid name for a json property
* @returns {string} the label name as a text with whitespaces
*/
function attributeNameToLabelName(attributeName) {
let labelName = attributeName.charAt(0).toUpperCase();
let nextCharToUpperCase = false;
for (let i = 1; i < attributeName.length; i++) {
let curChar = attributeName.charAt(i);
if (curChar < 'a' || curChar > 'z') {
if (curChar == '_') {
labelName += ' ';
nextCharToUpperCase = true;
}
else if (curChar >= 'A' || curChar <= 'Z')
labelName = labelName + ' ' + curChar.toUpperCase();
else
labelName += curChar;
} else {
labelName += nextCharToUpperCase ? curChar.toUpperCase() : curChar;
nextCharToUpperCase = false;
}
}
return labelName;
}
/**
* adds a text label as a child to the specified GUI element
* @param {dat.GUI} toBeAddedTo the gui object the label is added to
* @param {string} labelName the name of the gui variable representing this label
* @param {string} labelAlias the name of the label which should be displayed
*/
function addLabelToGUI(toBeAddedTo, labelName, labelAlias) {
let lbl = toBeAddedTo.add(guiController, labelName).listen();
if (labelAlias != undefined)
lbl.name(labelAlias);
lbl.domElement.style.pointerEvents = "none";
}
/**
* updates the GUI node data attributes with the values of the corresponding node
* @param {node} node the node whose attributes are shown in the GUI
*/
function guiUpdateSelectedNode(node) {
nodeDescriptionAttributes.forEach(nodeDescriptionAttribute => guiController[nodeDescriptionAttribute] = node[nodeDescriptionAttribute]);
}
/**
* updates the GUI node data attributes to empty values
*/
function resetSelectedNode() {
nodeDescriptionAttributes.forEach(nodeDescriptionAttribute => guiController[nodeDescriptionAttribute] = '-');
}
/**
* updates the GUI with the found node titles based on the current search term
* matching titles are all which contain the search term not regarding case sensitivity
* all previously found titles are removed first
*/
function guiUpdateSearchResults() {
deleteSearchResults();
// find nodes which match search
console.log('Searching nodes for: ' + guiController.searchTerm);
let searchWord = guiController.searchTerm.toLowerCase();
let searchSupernodes = (detailLODEnabled ? supernodes : allSupernodes);
searchSupernodes.forEach(supernode => {
supernode.nodeIds.forEach(nodeId => {
if (nodes[nodeId].title.toLowerCase().includes(searchWord)) {
console.log('Match found: ' + nodes[nodeId].title);
searchResultIdsPrev.push(nodeId);
}
})
})
// add found nodes to GUI and link to select node when clicked
let titles = [];
searchResultIdsPrev.forEach(searchResultIdPrev => {
guiController[nodes[searchResultIdPrev].title] = function () {
if (detailLODEnabled) {
// select searched node directly in detail LOD
selectNode(nodes[searchResultIdPrev]);
guiUpdateSelectedNode(nodes[searchResultIdPrev]);
}
else {
// select supernode containing searched node in low detail LOD
let containingSupernode;
allSupernodes.some(supernode => {
if (supernode.nodeIds.includes(searchResultIdPrev)) {
containingSupernode = supernode;
return true;
}
});
selectSupernode(containingSupernode);
}
};
titles.push(nodes[searchResultIdPrev].title);
})
// add found titles in alphabetic order
titles.sort((a, b) => a.localeCompare(b));
titles.forEach(title => {
let button = searchResultsFolder.add(guiController, title);
searchResultIdsPrevGUI.push(button);
});
}
/**
* removes all previously found search results from the GUI
*/
function deleteSearchResults() {
// delete previous attributes
searchResultIdsPrev.forEach(searchResultIdPrev => delete guiController[nodes[searchResultIdPrev].title]);
searchResultIdsPrevGUI.forEach(searchResultIdPrevGUI => searchResultsFolder.remove(searchResultIdPrevGUI));
searchResultIdsPrev = [];
searchResultIdsPrevGUI = [];
}
/**
* updates the GUI control parameters of the selected edge
* @param {edge} edge whose attributes should be shown
*/
function guiUpdateSelectedEdge(edge) {
resetSelectedEdge();
guiController.node1 = nodes[edge.id1].title;
guiController.node2 = nodes[edge.id2].title;
guiController.weight = edge.weight;
sharedEdgeAttributesPrev = computeSharedEdgeAttributes(edge);
sharedEdgeAttributesPrev.forEach(sharedEdgeAttributePrev => {
guiController[sharedEdgeAttributePrev] = emptyFunction;
sharedEdgeAttributesPrevGUI.push(sharedEdgeValuesFolder.add(guiController, sharedEdgeAttributePrev));
})
}
/**
* resets the GUI controls of the selected edge to empty values
*/
function resetSelectedEdge() {
guiController.node1 = '-';
guiController.node2 = '-';
guiController.weight = '-';
// delete previous shared edge attributes
sharedEdgeAttributesPrev.forEach(sharedEdgeAttributePrev => delete guiController[sharedEdgeAttributePrev]);
sharedEdgeAttributesPrevGUI.forEach(sharedEdgeAttributePrevGUI => sharedEdgeValuesFolder.remove(sharedEdgeAttributePrevGUI));
sharedEdgeAttributesPrevGUI = [];
sharedEdgeAttributesPrev = [];
}
/**
* computes all shared values between the two endnodes of the edge regarding the active edge set
* @param {edge} edge to be considered
* @returns {string[]} the common attributes on the edge
*/
function computeSharedEdgeAttributes(edge) {
let node1Values = nodes[edge.id1][edgeSetName];
let node2Values = nodes[edge.id2][edgeSetName];
return node1Values.filter(value => node2Values.includes(value));
}
/**
* sorts the nodes stored in each of the currently active supernodes based on the active node sorting attribute
*/
function sortNodesInSupernodes() {
let renderedNodes = [];
supernodes.forEach(supernode => {
supernode.nodeIds.forEach(nodeId => renderedNodes.push(nodes[nodeId]));
})
supernodesSortingValueMax = renderedNodes.reduce((max, node) => Math.max(max, node[nodesSortingAttribute]), 0);
supernodesSortingValueMin = renderedNodes.reduce((min, node) => Math.min(min, node[nodesSortingAttribute]), Number.MAX_VALUE);
supernodes.forEach(supern => {
supern.nodeIds.sort((nodeId1, nodeId2) => {
return nodes[nodeId2][nodesSortingAttribute] - nodes[nodeId1][nodesSortingAttribute];
})
});
}
/**
* updates the GUI when an obvserved value has changed
*/
var update = function () {
requestAnimationFrame(update);
};
/**
* returns the attribute name of the currently active edge set
* @returns {string} the name of the selected edge set
*/
function getSelectedEdgeSet() {
if (edgeSetName == 'cast') {
return "castEdges";
}
else if (edgeSetName == 'keywords') {
return "keywordsEdges";
}
else if (edgeSetName == 'genres') {
return "genresEdges";
}
return undefined;
}