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