Source: gui.js

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