Source: renderLogic.js

/**
 * Handles all OpenGL objects for rendering and the logic for the rendering
 * @module RenderLogic
 */

var nodesNumberEdges = {};
var supernodesNumberEdgesOutside = [];
var supernodesNumberEdgesInside = [];

var emptySupernodeRadiusInsideMin = 0.8;
var emptySupernodeRadiusInside;
var supernodeRadius;
var supernodeSizePercentage = 0.15;
var supernodeZPosition = 0;
var supernodesEdgeWeightMax;
var supernodesEdgeWeightMin;
var supernodesSortingValueMax;
var supernodesSortingValueMin;
var supernodeEdgeMatrix;
var lowDetailSupernodeEdgeMatrix;
var lowDetailSupernodePositions;

var allSupernodesElementsMax;
var allSupernodesElementsMin;
var allSupernodesWeightMin;
var allSupernodesWeightMax;
var allSupernodesClosestDistance;
var supernodeEdgeLineIndices = {};
var prevClickedSupernodeEdgeNames = [];

var wordCloudMaterials = {};
var wordCloudRenderNames = [];

var prevClickedRenderedEdge;
var prevClickedNode;
var clickedSupernodes = [];
var nodeGroup = new THREE.Group();
var linesGroup = new THREE.Group();
var allocatedGeometries = [];

var nodeGeometries = {};
var supernodeGeometries = {};

/**
 * initializes, constructs and renders a new subset of supernodes based on the current value of supernodes
 */
function setupNewDetailLODAndRender() {
    setupDetailLODMetaParameters();
    setupDetailLODAndRender();
}

/**
 * constructs and renders the subset of supernodes which has been rendered last based on the current value of supernodes
 * also renders the word clouds inside of the supernodes
 */
function setupDetailLODAndWordCloudsAndRender() {
    setupDetailLODAndRender();
    createWordCloudRenderObjects();
}


/**
 * constructs and renders the subset of supernodes which has been rendered last based on the current value of supernodes
 */
function setupDetailLODAndRender() {
    cleanupScene();
    setupDetailLODRenderObjects();
    renderScene();
    hideLoadingLabel();
}

/**
 *  initializes the parameters needed to render a specific set of supernodes.
 */
function setupDetailLODMetaParameters() {
    let edgeMatrixAndWeights = edgeMatrices[getSelectedEdgeSet()];
    supernodeEdgeMatrix = edgeMatrixAndWeights["edgeMatrix"];
    supernodesEdgeWeightMin = edgeMatrixAndWeights["minEdgeWeight"];
    supernodesEdgeWeightMax = edgeMatrixAndWeights["maxEdgeWeight"];
    updateGUIEdgeWeightLimits(supernodesEdgeWeightMin, supernodesEdgeWeightMax, true);
}

/**
 * initializes, constructs and renders newly clustered supernodes based on the current cluster settings
 */
function setupNewLowDetailLODAndRender() {
    setupLowDetailLODMetaParameters();
    setupLowDetailLODAndRender();
}

/**
 * constructs and renders already clustered supernodes based on the current cluster settings
 */
function setupLowDetailLODAndRender() {
    cleanupScene();
    setupLowDetailLODRenderObjects();
    renderScene();
    hideLoadingLabel();
}

/**
 *  initializes the parameters needed to render a specific supernode clustering.
 */
function setupLowDetailLODMetaParameters() {
    edgeSet = getSelectedEdgeSet();
    lowDetailSupernodeEdgeMatrix = lowDetailSuperNodeEdgeMatrices[edgeSet];
    //positions generated using networkx for low detail view of supernodes
    lowDetailSupernodePositions = lowDetailSuperNodePositionsMultipleEdgeSets[edgeSet];
    // compute max and min count of nodes in a supernode
    allSupernodesElementsMax = allSupernodes.reduce((max, supernode) => Math.max(max, supernode.nodeIds.length), 0);
    allSupernodesElementsMin = allSupernodes.reduce((min, supernode) => Math.min(min, supernode.nodeIds.length), Number.MAX_VALUE);
    // compute min and max values of edge weights for proper coloring
    allSupernodesWeightMax = -Number.MAX_VALUE;
    allSupernodesWeightMin = Number.MAX_VALUE;
    allSupernodesClosestDistance = Number.MAX_VALUE;
    for (let i = 0; i < lowDetailSupernodeEdgeMatrix.length; i++) {
        for (let j = i; j < lowDetailSupernodeEdgeMatrix[i].length; j++) {
            if (allSupernodesWeightMax < lowDetailSupernodeEdgeMatrix[i][j])
                allSupernodesWeightMax = lowDetailSupernodeEdgeMatrix[i][j];
            else if (allSupernodesWeightMin > lowDetailSupernodeEdgeMatrix[i][j])
                allSupernodesWeightMin = lowDetailSupernodeEdgeMatrix[i][j];
            if (i != j) {
                let distanceX = lowDetailSupernodePositions[i][0] - lowDetailSupernodePositions[j][0];
                let distanceY = lowDetailSupernodePositions[i][1] - lowDetailSupernodePositions[j][1];
                let distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
                if (distance < allSupernodesClosestDistance)
                    allSupernodesClosestDistance = distance;
            }
        }
    }
}

/**
 * clears the entire rendered scene and deletes all geometries on the GPU
 * clears all selections
 */
function cleanupScene() {
    if (scene.children.length != 0) {
        scene.dispose();
        while (scene.children.length > 0)
            scene.remove(scene.children[0]);
        nodeGroup = new THREE.Group();
        lines = [];
        linesGroup = new THREE.Group();
        prevClickedRenderedEdge = undefined;
        prevClickedNode = undefined;
        prevClickedSupernodeEdgeNames;
        clickedSupernodes = [];
        // delte geometries from GPU
        allocatedGeometries.forEach(allocatedGeometry => allocatedGeometry.dispose());
        allocatedGeometries = [];
        wordCloudRenderNames = [];
        // delete all previous node geometries
        nodeGeometries = {};
        supernodeGeometries = {};
    }
}

/**
 * creates all OpenGL objects required to render a specifc detailLOD with the currently active supernodes
 * renders the scene afterwards
 */
function setupDetailLODRenderObjects() {
    // sort nodes based on selected attribute
    sortNodesInSupernodes();
    // compute which nodes/ supernodes have how many internal/external edges for layouting
    computeNodesNumberEdges(supernodeEdgeMatrix);
    // compute rendering scale of supernodes and the distance between them based on the edges
    let supernodesDistance = computeRenderingScales();
    // compute layout of the supernodes depending on how many supernodes are selected
    let cameraSize = computeSupernodeRenderingPositions(supernodesDistance);
    // adjust camera view and interaction parameters to match visualization
    updateCamera(cameraSize, 1.0, true);
    // create the actual openGL buffers etc. for the supernodes for rendering
    computeDetailLODSupernodeRenderingObjects();
    // compute the edge matrix for rendering
    // it stores the start and end positions of the edges for rendering and other relevant information
    var supernodeRenderedEdgeMatrix = computeSupernodeRenderedEdgeMatrix(supernodeEdgeMatrix, guiController.minEdgeWeight, guiController.maxEdgeWeight);
    // compute the trajectories of the lines for the edges
    // this is the core layouting algorithm
    computeDetailLODLinesRenderingObject(supernodeRenderedEdgeMatrix);
    // create the openGL buffer for the line rendering. (all lines are rendered via 1 buffer/ draw call)
    createLinesRenderingObject(computeEdgeColorForDetailLOD, edgeMaterial);
}

/**
 * creates all OpenGL objects required to render a specifc lowDetailLOD with the currently active cluster settings
 * renders the scene afterwards
 */
function setupLowDetailLODRenderObjects() {
    // calculate the size of each node for rendering and position camera to fit the visualization
    let supernodeSize = computeLowDetailLODCameraSettings();
    // create the actual openGL buffers etc. for the supernodes for rendering
    computeLowDetailLODSupernodeRenderingObjects(supernodeSize);
    // create one array storing all the edge positions
    computeLowDetailLODLinesRenderingObject();
    // create one opengl buffer for all edges
    createLinesRenderingObject(computeEdgeColorForLowDetailLOD, edgeMaterial);
}

/**
 * positions the camera to show the entire lowDetailLOD
 * @returns size in which the supernodes in the lowDetailLOD should be rendered
 */
function computeLowDetailLODCameraSettings() {
    let maxX = lowDetailSupernodePositions.reduce((max, position) => Math.max(max, position[0]), -Number.MAX_VALUE);
    let minX = lowDetailSupernodePositions.reduce((min, position) => Math.min(min, position[0]), Number.MAX_VALUE);
    let maxY = lowDetailSupernodePositions.reduce((max, position) => Math.max(max, position[1]), -Number.MAX_VALUE);
    let minY = lowDetailSupernodePositions.reduce((min, position) => Math.min(min, position[1]), Number.MAX_VALUE);
    let cameraSize = Math.max(maxX - minX, maxY - minY);
    updateCamera(cameraSize, 1.0, true);
    let supernodeSize = Math.min(cameraSize / 100, allSupernodesClosestDistance / 2 * 0.9);
    return supernodeSize;
}

/**
 * creates the OpenGL objects for the supernodes of the lowDetailLOD and adds them to the scene
 * @param {float} supernodeSize size of the supernodes at which they are rendered
 */
function computeLowDetailLODSupernodeRenderingObjects(supernodeSize) {
    allSupernodes.forEach((supernode, index) => {
        let supernodePos = { x: lowDetailSupernodePositions[index][0], y: lowDetailSupernodePositions[index][1], z: supernodeZPosition };
        // add main ring for each supernode showing how many internal edges it has
        var supernodeGeometry = new THREE.CircleGeometry(supernodeSize * 0.9, 36);
        allocatedGeometries.push(supernodeGeometry);
        let col = computeColor(lowDetailSupernodeEdgeMatrix[supernode.id][supernode.id], allSupernodesWeightMin, allSupernodesWeightMax);
        supernodeGeometry.faces.forEach(face => face.color = col);
        // store supernodeid to obtain node object when node geometry has been clicked later
        supernodeGeometry.supernodeId = supernode.id;
        // also store reference to node geometry for each node
        supernodeGeometries[supernode.id] = supernodeGeometry;
        var supernodeGeometryMesh = createMeshAtFixedPosition(supernodeGeometry, coloredBasicMeshMaterial, supernodePos);
        nodeGroup.add(supernodeGeometryMesh);

        // add border for each supernode
        var supernodeOutlineGeometry = new THREE.CircleGeometry(supernodeSize, 36);
        allocatedGeometries.push(supernodeOutlineGeometry);
        supernodePos.z = supernodePos.z - 0.1;
        var supernodeOutlineGeometryMesh = createMeshAtFixedPosition(supernodeOutlineGeometry, outlineMaterial, supernodePos);
        scene.add(supernodeOutlineGeometryMesh);

        // add inside ring for each supernode showing how many nodes are part of it
        var supernodeInsideGeometry = new THREE.CircleGeometry(supernodeSize / 2, 36);
        allocatedGeometries.push(supernodeInsideGeometry);
        col = computeColor(supernode.nodeIds.length, allSupernodesElementsMin, allSupernodesElementsMax);
        supernodeInsideGeometry.faces.forEach(face => face.color = col);
        supernodePos.z = supernodePos.z + 0.2;
        var supernodeInsideGeometryMesh = createMeshAtFixedPosition(supernodeInsideGeometry, coloredBasicMeshMaterial, supernodePos);
        scene.add(supernodeInsideGeometryMesh);
    });
    scene.add(nodeGroup);
}

/**
 * creates the OpenGL object for the edges between the supernodes of the lowDetailLOD and adds them to the scene
 */
function computeLowDetailLODLinesRenderingObject() {
    // store where in the OpenGL buffer each edge is located
    supernodeEdgeLineIndices = {};
    for (let i = 0; i < lowDetailSupernodeEdgeMatrix.length; i++) {
        for (let j = i + 1; j < lowDetailSupernodeEdgeMatrix[i].length; j++) {
            if (lowDetailSupernodeEdgeMatrix[i][j] > 0) {
                let zPos = edgeZPositionMin + lowDetailSupernodeEdgeMatrix[i][j] / allSupernodesWeightMax * edgeZPositionRange;
                let key = allSupernodes[i].id + ',' + allSupernodes[j].id;
                supernodeEdgeLineIndices[key] = lines.length;
                lines.push({
                    position1: { x: lowDetailSupernodePositions[i][0], y: lowDetailSupernodePositions[i][1], z: zPos },
                    position2: { x: lowDetailSupernodePositions[j][0], y: lowDetailSupernodePositions[j][1], z: zPos },
                    weight: lowDetailSupernodeEdgeMatrix[i][j]
                });
            }
        }
    }
}

/**
 * constructs and places a new mesh which stays at its place staticly
 * @param {THREE.Geometry} geometry from which to construct the mesh
 * @param {THREE.Material} material from which to construct the mesh
 * @param {vec3} position at which to place the mesh
 * @returns {THREE.Mesh} created mesh
 */
function createMeshAtFixedPosition(geometry, material, position) {
    var mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(position.x, position.y, position.z);
    mesh.updateMatrix();
    mesh.matrixAutoUpdate = false;
    return mesh;
}

/**
 * computes the size of the supernodes at which they are rendered in the detailLOD
 * @returns {float} used distance between rendered supernodes
 */
function computeRenderingScales() {
    let maxSupernodeEdgesInside = supernodesNumberEdgesInside.reduce((max, supernodeNumberEdgesInside) => Math.max(max, supernodeNumberEdgesInside), 0);
    emptySupernodeRadiusInside = Math.max(emptySupernodeRadiusInsideMin, maxSupernodeEdgesInside * edgeDistanceMin * 2);
    edgeDistance = emptySupernodeRadiusInside * 0.0125;
    supernodeRadius = emptySupernodeRadiusInside + maxSupernodeEdgesInside * edgeDistance + edgeLengthNodeOut;
    supernodeRadius = supernodeRadius / (1.0 - supernodeSizePercentage / 2); // adjust radius because of width of ring segments

    let maxSupernodeEdgesOutside = supernodesNumberEdgesOutside.reduce((max, supernodeNumberEdgesOutside) => Math.max(max, supernodeNumberEdgesOutside), 0);
    let supernodesDistance = (supernodeRadius + edgeLengthNodeOut + maxSupernodeEdgesOutside * edgeDistance) * 3;
    return supernodesDistance;
}

/**
 * sets the rendering positions of the selected supernodes for the detailLOD
 * @param {float} supernodesDistance distance between the rendered supernodes
 * @returns {float} margin factor to use for setting the camera size
 */
function computeSupernodeRenderingPositions(supernodesDistance) {
    let halfSupernodesDistance = supernodesDistance / 2;
    let marginFactor = 1.1;
    if (supernodes.length == 1) {
        setSupernodePosition(supernodes[0], { x: 0, y: 0, z: supernodeZPosition });
        return supernodesDistance * marginFactor;
    }
    else if (supernodes.length == 2) {
        setSupernodePosition(supernodes[0], { x: -halfSupernodesDistance, y: 0, z: supernodeZPosition });
        setSupernodePosition(supernodes[1], { x: halfSupernodesDistance, y: 0, z: supernodeZPosition });
        return (supernodesDistance * 4 / 3) * marginFactor;
    }
    else if (supernodes.length == 3) {
        let halfHeight = Math.sqrt(Math.pow(supernodesDistance, 2) - Math.pow(halfSupernodesDistance, 2)) / 2;
        setSupernodePosition(supernodes[0], { x: -halfSupernodesDistance, y: -halfHeight, z: supernodeZPosition });
        setSupernodePosition(supernodes[1], { x: halfSupernodesDistance, y: -halfHeight, z: supernodeZPosition });
        setSupernodePosition(supernodes[2], { x: 0, y: halfHeight, z: supernodeZPosition });
        return (supernodesDistance * 5 / 3) * marginFactor;
    }
    else if (supernodes.length == 4) {
        setSupernodePosition(supernodes[0], { x: -halfSupernodesDistance, y: -halfSupernodesDistance, z: supernodeZPosition });
        setSupernodePosition(supernodes[1], { x: halfSupernodesDistance, y: -halfSupernodesDistance, z: supernodeZPosition });
        setSupernodePosition(supernodes[2], { x: halfSupernodesDistance, y: halfSupernodesDistance, z: supernodeZPosition });
        setSupernodePosition(supernodes[3], { x: -halfSupernodesDistance, y: halfSupernodesDistance, z: supernodeZPosition });
        return (supernodesDistance * 2) * marginFactor;
    }
    return marginFactor;
}

/**
 * Sets the position of a supernode
 *
 * @param {supernode} supern the supernode
 * @param {vec3} position the absolute position of the node
 */
function setSupernodePosition(supern, position) {
    supern.position = position;
}

/**
 * computes the position of a node in the detailLOD
 * @param {int} supernodeIndex index of the supernode the node belongs to
 * @param {int} nodeIndex where the node is positioned within the supernode
 * @returns {vec3} the absolute position of the node
 */
function computeNodePosition(supernodeIndex, nodeIndex) {
    let supernode = supernodes[supernodeIndex];
    let angle = computeNodeAngle(supernodeIndex, nodeIndex);
    return { x: supernode.position.x + supernodeRadius * Math.cos(angle), y: supernode.position.y + supernodeRadius * Math.sin(angle), z: supernode.position.z };
}

/**
 * computes the total number of edges for each node in the current detailLOD (inter und intra)
 * computes the total number of edges for each supernode in the current detailLOD (inter und intra)
 * @param {edge[][][]} supernodeEdgeMatrix
 */
function computeNodesNumberEdges(supernodeEdgeMatrix) {
    nodesNumberEdges = {};
    supernodesNumberEdgesInside = [];
    supernodesNumberEdgesOutside = [];
    supernodes.forEach((supernode, index) => {
        supernodesNumberEdgesOutside[index] = 0;
        supernodesNumberEdgesInside[index] = 0;
    });
    for (let i = 0; i < supernodes.length; i++) {
        for (let j = i; j < supernodes.length; j++) {
            for (let k = 0; k < supernodeEdgeMatrix[i][j].length; k++) {
                let edge = supernodeEdgeMatrix[i][j][k];
                if (nodesNumberEdges[edge.id1] == undefined)
                    nodesNumberEdges[edge.id1] = { inside: { total: 0, count: 0 }, outside: { total: 0, count: 0 } };
                if (nodesNumberEdges[edge.id2] == undefined)
                    nodesNumberEdges[edge.id2] = { inside: { total: 0, count: 0 }, outside: { total: 0, count: 0 } };
                if (i != j) {
                    nodesNumberEdges[edge.id1].outside.total++;
                    nodesNumberEdges[edge.id2].outside.total++;
                    supernodesNumberEdgesOutside[i]++;
                    supernodesNumberEdgesOutside[j]++;
                }
                else {
                    nodesNumberEdges[edge.id1].inside.total++;
                    nodesNumberEdges[edge.id2].inside.total++;
                    supernodesNumberEdgesInside[i]++;
                }
            }
        }
    }
}

/**
 * creates the OpenGL buffer and line segments for all elements currently stored in the lines array
 * the created mesh (lines) is added to the scene
 * @param {function(float)} edgeColoringFunction that returns a THREE.Color based on a weight
 * @param {THREE.material} material used to render the lines
 */
function createLinesRenderingObject(edgeColoringFunction, material) {
    let numPoints = lines.length * 2;
    var positions = new Float32Array(numPoints * 3); // 3 vertices per point
    var colors = new Float32Array(numPoints * 3); // 3 channels per point

    var geometry = new THREE.BufferGeometry();
    allocatedGeometries.push(geometry);
    geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
    geometry.getAttribute('color').dynamic = true;
    geometry.getAttribute('position').dynamic = true;
    let lineCounter = 0;
    for (var index = 0; index < numPoints * 3; index += 6) {
        positions[index] = lines[lineCounter].position1.x;
        positions[index + 1] = lines[lineCounter].position1.y;
        positions[index + 2] = lines[lineCounter].position1.z;
        positions[index + 3] = lines[lineCounter].position2.x;
        positions[index + 4] = lines[lineCounter].position2.y;
        positions[index + 5] = lines[lineCounter].position2.z;
        let col = edgeColoringFunction(lines[lineCounter].weight);

        lines[lineCounter].color = col;
        colors[index] = col.r;
        colors[index + 1] = col.g;
        colors[index + 2] = col.b;
        colors[index + 3] = col.r;
        colors[index + 4] = col.g;
        colors[index + 5] = col.b;

        lineCounter++;
    }
    let linesMesh = new THREE.LineSegments(geometry, material);
    linesGroup.add(linesMesh);
    scene.add(linesGroup);
}

/**
 * Computer the color of the detailed view edges
 *
 * @param {int} weight the edge weight
 * @returns {THREE.Color} the computed color
 */
function computeEdgeColorForDetailLOD(weight) {
    return computeColor(weight, supernodesEdgeWeightMin, supernodesEdgeWeightMax);
}

/**
 * Computer the color of the low detail view edges
 *
 * @param {int} weight the edge weight
 * @returns {THREE.Color} the computed color
 */
function computeEdgeColorForLowDetailLOD(weight) {
    return computeColor(weight, allSupernodesWeightMin, allSupernodesWeightMax);
}

/**
 * creates all supernode OpenGL objects of the currently active detailLOD and adds them to the scene
 */
function computeDetailLODSupernodeRenderingObjects() {
    supernodes.forEach(supernode => createSupernodeRenderingObject(supernode));
    scene.add(nodeGroup);
}

/**
 * creates the supernode OpenGL objects of the specified supernode
 * @param {supernode} supernode
 */
function createSupernodeRenderingObject(supernode) {
    let segmentAngle = Math.PI * 2 / supernode.nodeIds.length;
    let circularGap = Math.PI / 150;
    let supernodeThickness = supernodeSizePercentage * supernodeRadius;
    let supernodeOutlineThickness = supernodeThickness * 0.125;
    // create one ring segment for every node
    for (let i = 0; i < supernode.nodeIds.length; i++) {
        var ringSegment = new THREE.RingGeometry(supernodeRadius - supernodeThickness / 2 + supernodeOutlineThickness, supernodeRadius + supernodeThickness / 2 - supernodeOutlineThickness,
            Math.ceil(segmentAngle / circleAngleStep), 1, i * segmentAngle + circularGap / 2, segmentAngle - circularGap);
        allocatedGeometries.push(ringSegment);
        let col = computeColor(nodes[supernode.nodeIds[i]][nodesSortingAttribute], supernodesSortingValueMin, supernodesSortingValueMax);
        ringSegment.faces.forEach(face => face.color = col);
        // store nodeid to obtain node object when node geometry has been clicked later
        ringSegment.nodeId = supernode.nodeIds[i];
        // also store reference to node geometry for each node
        nodeGeometries[supernode.nodeIds[i]] = ringSegment;
        var ringSegmentMesh = createMeshAtFixedPosition(ringSegment, coloredBasicMeshMaterial, supernode.position);
        nodeGroup.add(ringSegmentMesh);
    }
    // create one ring as background behind nodes
    var ringSegmentOutline = new THREE.RingGeometry(supernodeRadius - supernodeThickness / 2, supernodeRadius + supernodeThickness / 2,
        Math.ceil(Math.PI * 2 / circleAngleStep), 1, 0, 2 * Math.PI);
    allocatedGeometries.push(ringSegmentOutline);
    let pos = { x: supernode.position.x, y: supernode.position.y, z: supernode.position.z - 0.1 };
    var ringSegmentOutlineMesh = createMeshAtFixedPosition(ringSegmentOutline, outlineMaterial, pos)
    scene.add(ringSegmentOutlineMesh);
}

function computeEdgeZPriority(weight, maxWeight) {
    return weight / maxWeight;
}

/**
 * computes the color in which the nodes and edges are rendered based on the specified weights
 * it uses linear interpolation
 * @param {float} weight
 * @param {float} minWeight
 * @param {float} maxWeight
 * @returns {THREE.Color} computed color
 */
function computeColor(weight, minWeight, maxWeight) {
    let percent = 1;
    if (maxWeight > minWeight)
        percent = (weight - minWeight) / (maxWeight - minWeight);
    let col = new THREE.Color().set(colorWeak);
    return col.lerpHSL(new THREE.Color().set(colorMedium), percent);
}

/**
 * highlights the specified node in the scene and unhighlights the previously specified node
 * @param {node} node to be highlighted
 */
function selectNode(node) {
    if (prevClickedNode != undefined) {
        colorNode(prevClickedNode, computeColor(prevClickedNode[nodesSortingAttribute], supernodesSortingValueMin, supernodesSortingValueMax));
    }
    colorNode(node, clickedColor);
    renderScene();
    console.log('clicked node: ' + node.title);
    prevClickedNode = node;
}

/**
 * highlights/ unhighlights the specified supernode in the scene
 * a maximum of 4 supernodes can be highlighted at once
 * further highlighting requests will be ignored until a spot is available again
 * edges connecting the highlighted supernodes are also highlighted
 * @param {supernode} supernode to be highlighted
 */
function selectSupernode(supernode) {
    let selectionUpdated = false;
    if (clickedSupernodes.includes(supernode)) {
        clickedSupernodes.splice(clickedSupernodes.indexOf(supernode), 1);
        colorSupernode(supernode, computeColor(lowDetailSupernodeEdgeMatrix[supernode.id][supernode.id], allSupernodesWeightMin, allSupernodesWeightMax));
        selectionUpdated = true;
    }
    else if (clickedSupernodes.length < 4) {
        clickedSupernodes.push(supernode);
        colorSupernode(supernode, clickedColor);
        selectionUpdated = true;
    }
    else {
        console.log('Cannot select more than 4 supernodes at once');
    }
    if (selectionUpdated) {
        // unhighlight previous edges connecting selected nodes
        prevClickedSupernodeEdgeNames.forEach(prevClickedSupernodeEdgeName => {
            let id1 = prevClickedSupernodeEdgeName.substring(0, prevClickedSupernodeEdgeName.indexOf(','));
            let id2 = prevClickedSupernodeEdgeName.substring(prevClickedSupernodeEdgeName.indexOf(',') + 1);
            let supernodeEdgeWeight = lowDetailSupernodeEdgeMatrix[id1][id2];
            let col = computeEdgeColorForLowDetailLOD(supernodeEdgeWeight);
            colorEdge(supernodeEdgeLineIndices[prevClickedSupernodeEdgeName], supernodeEdgeLineIndices[prevClickedSupernodeEdgeName] + 1, supernodeEdgeWeight, allSupernodesWeightMax, col);
        });
        prevClickedSupernodeEdgeNames = [];
        // highlight edges connecting selected nodes
        for (let i = 0; i < clickedSupernodes.length; i++) {
            for (let j = i + 1; j < clickedSupernodes.length; j++) {
                let id1 = Math.min(clickedSupernodes[i].id, clickedSupernodes[j].id);
                let id2 = Math.max(clickedSupernodes[i].id, clickedSupernodes[j].id);
                let supernodeEdgeWeight = lowDetailSupernodeEdgeMatrix[id1][id2];
                if (supernodeEdgeWeight > 0) {
                    let key = id1 + ',' + id2;
                    prevClickedSupernodeEdgeNames.push(key);
                    colorEdge(supernodeEdgeLineIndices[key], supernodeEdgeLineIndices[key] + 1, supernodeEdgeWeight, allSupernodesWeightMax, clickedColor);
                }
            }
        }
    }
    renderScene();
    console.log('clicked supernode: ' + supernode.id);
}
/**
 * highlights the specified edge in the scene and unhighlights the previously specified edge
 * @param {renderedEdge} renderedEdge to be highlighted
 */
function selectEdge(renderedEdge) {
    if (prevClickedRenderedEdge != undefined) {
        colorDetailLODEdge(prevClickedRenderedEdge, computeColor(prevClickedRenderedEdge.edge.weight, supernodesEdgeWeightMin, supernodesEdgeWeightMax));
    }
    colorDetailLODEdge(renderedEdge, clickedColor);
    renderScene();
    console.log('clicked edge: ' + nodes[renderedEdge.edge.id1].title + '; ' + nodes[renderedEdge.edge.id2].title);
    prevClickedRenderedEdge = renderedEdge;
}

/**
 * colors the specified node in the specified color
 * @param {node} node to be colored
 * @param {THREE.Color} color to be used
 */
function colorNode(node, color) {
    let nodeGeometry = nodeGeometries[node.id];
    nodeGeometry.faces.forEach(face => face.color = color);
    nodeGeometry.elementsNeedUpdate = true;
}

/**
 * colors the specified supernode in the specified color
 * @param {supernode} supernode to be colored
 * @param {THREE.Color} color to be used
 */
function colorSupernode(supernode, color) {
    let nodeGeometry = supernodeGeometries[supernode.id];
    nodeGeometry.faces.forEach(face => face.color = color);
    nodeGeometry.elementsNeedUpdate = true;
}

/**
 * colors the specified edge in the specified color
 * @param {renderedEdge} renderedEdge to be colored
 * @param {THREE.Color} color to be used
 */
function colorDetailLODEdge(renderedEdge, color) {
    colorEdge(renderedEdge.linesStartIndex, renderedEdge.linesEndIndex, renderedEdge.edge.weight, supernodesEdgeWeightMax, color);
}

/**
 * colors the specified edge which is defined by the start and end indices in the specified color
 * @param {int} linesStartIndex of the edge to be colored in the lines array
 * @param {int} linesEndIndex of the edge to be colored in the lines array
 * @param {float} weight of the edge to be colored
 * @param {float} maxWeight of all edges in the current view
 * @param {THREE.color} color to be used
 */
function colorEdge(linesStartIndex, linesEndIndex, weight, maxWeight, color) {
    let col = linesGroup.children[0].geometry.getAttribute('color');
    let pos = linesGroup.children[0].geometry.getAttribute('position');
    // colIndex *2 because there are 2points/line and *3 because there are 3floats/color
    let colStartIndex = linesStartIndex * 2 * 3;
    let colEndIndex = linesEndIndex * 2 * 3;
    col.needsUpdate = true;
    pos.needsUpdate = true;
    let zValue;
    if (color == clickedColor)
        zValue = computeEdgeZPosition(1.05);
    else
        zValue = computeEdgeZPosition(computeEdgeZPriority(weight, maxWeight));
    for (let i = colStartIndex; i < colEndIndex; i += 3) {
        col.array[i] = color.r;
        col.array[i + 1] = color.g;
        col.array[i + 2] = color.b;
        pos.array[i + 2] = zValue;
    }
}

/**
 * creates the OpenGL objects for the word clouds and reders the scene
 * the old word clouds are deleted
 */
function createWordCloudRenderObjects() {
    deleteOldWordClouds();

    supernodes.forEach(function (supernode) {
        const geometry = new THREE.CircleGeometry(emptySupernodeRadiusInside, 32);
        const material = wordCloudMaterials[supernode["id"]];
        const plane = new THREE.Mesh(geometry, material);
        plane.position.set(supernode.position.x, supernode.position.y, supernode.position.z);
        wordCloudRenderNames.push(supernode["id"]);
        plane.name = supernode["id"];
        plane.material.map.needsUpdate = true;
        scene.add(plane);
    });
    renderScene();
}

/**
 * delete existing word cloud OpenGL objects and js obects
 */
function deleteOldWordClouds() {
    let names = wordCloudRenderNames;
    names.forEach(function (name, i) {
        var oldWordCloud = scene.getObjectByName(name);
        scene.remove(oldWordCloud);
        if (i == names.length - 1) {
            wordCloudRenderNames = [];
        }
    });
}