import Texture2D from 'claygl/src/Texture2D'; import Texture from 'claygl/src/Texture'; import workerFunc from './forceAtlas2Worker.js'; var workerUrl = workerFunc.toString(); workerUrl = workerUrl.slice(workerUrl.indexOf('{') + 1, workerUrl.lastIndexOf('}')); var defaultConfigs = { barnesHutOptimize: true, barnesHutTheta: 1.5, repulsionByDegree: true, linLogMode: false, strongGravityMode: false, gravity: 1.0, scaling: 1.0, edgeWeightInfluence: 1.0, jitterTolerence: 0.1, preventOverlap: false, dissuadeHubs: false, gravityCenter: null }; var ForceAtlas2 = function (options) { for (var name in defaultConfigs) { this[name] = defaultConfigs[name]; } if (options) { for (var name in options) { this[name] = options[name]; } } this._nodes = []; this._edges = []; this._disposed = false; this._positionTex = new Texture2D({ type: Texture.FLOAT, flipY: false, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST }); }; ForceAtlas2.prototype.initData = function (nodes, edges) { var bb = new Blob([workerUrl]); var blobURL = window.URL.createObjectURL(bb); this._worker = new Worker(blobURL); this._worker.onmessage = this._$onupdate.bind(this); this._nodes = nodes; this._edges = edges; this._frame = 0; var nNodes = nodes.length; var nEdges = edges.length; var positionArr = new Float32Array(nNodes * 2); var massArr = new Float32Array(nNodes); var sizeArr = new Float32Array(nNodes); var edgeArr = new Float32Array(nEdges * 2); var edgeWeightArr = new Float32Array(nEdges); for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; positionArr[i * 2] = node.x; positionArr[i * 2 + 1] = node.y; massArr[i] = node.mass == null ? 1 : node.mass; sizeArr[i] = node.size == null ? 1 : node.size; } for (var i = 0; i < edges.length; i++) { var edge = edges[i]; var source = edge.node1; var target = edge.node2; edgeArr[i * 2] = source; edgeArr[i * 2 + 1] = target; edgeWeightArr[i] = edge.weight == null ? 1 : edge.weight; } var textureWidth = Math.ceil(Math.sqrt(nodes.length)); var textureHeight = textureWidth; var pixels = new Float32Array(textureWidth * textureHeight * 4); var positionTex = this._positionTex; positionTex.width = textureWidth; positionTex.height = textureHeight; positionTex.pixels = pixels; this._worker.postMessage({ cmd: 'init', nodesPosition: positionArr, nodesMass: massArr, nodesSize: sizeArr, edges: edgeArr, edgesWeight: edgeWeightArr }); this._globalSpeed = Infinity; }; ForceAtlas2.prototype.updateOption = function (options) { var config = {}; // Default config for (var name in defaultConfigs) { config[name] = defaultConfigs[name]; } var nodes = this._nodes; var edges = this._edges; // Config according to data scale var nNodes = nodes.length; if (nNodes > 50000) { config.jitterTolerence = 10; } else if (nNodes > 5000) { config.jitterTolerence = 1; } else { config.jitterTolerence = 0.1; } if (nNodes > 100) { config.scaling = 2.0; } else { config.scaling = 10.0; } if (nNodes > 1000) { config.barnesHutOptimize = true; } else { config.barnesHutOptimize = false; } if (options) { for (var name in defaultConfigs) { if (options[name] != null) { config[name] = options[name]; } } } if (!config.gravityCenter) { var min = [Infinity, Infinity]; var max = [-Infinity, -Infinity]; for (var i = 0; i < nodes.length; i++) { min[0] = Math.min(nodes[i].x, min[0]); min[1] = Math.min(nodes[i].y, min[1]); max[0] = Math.max(nodes[i].x, max[0]); max[1] = Math.max(nodes[i].y, max[1]); } config.gravityCenter = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5]; } // Update inDegree, outDegree for (var i = 0; i < edges.length; i++) { var node1 = edges[i].node1; var node2 = edges[i].node2; nodes[node1].degree = (nodes[node1].degree || 0) + 1; nodes[node2].degree = (nodes[node2].degree || 0) + 1; } if (this._worker) { this._worker.postMessage({ cmd: 'updateConfig', config: config }); } }; // Steps per call, to keep sync with rendering ForceAtlas2.prototype.update = function (renderer, steps, cb) { if (steps == null) { steps = 1; } steps = Math.max(steps, 1); this._frame += steps; this._onupdate = cb; if (this._worker) { this._worker.postMessage({ cmd: 'update', steps: Math.round(steps) }); } }; ForceAtlas2.prototype._$onupdate = function (e) { // Incase the worker keep postMessage of last frame after it is disposed if (this._disposed) { return; } var positionArr = new Float32Array(e.data.buffer); this._globalSpeed = e.data.globalSpeed; this._positionArr = positionArr; this._updateTexture(positionArr); this._onupdate && this._onupdate(); }; ForceAtlas2.prototype.getNodePositionTexture = function () { return this._positionTex; }; ForceAtlas2.prototype.getNodeUV = function (nodeIndex, uv) { uv = uv || []; var textureWidth = this._positionTex.width; var textureHeight = this._positionTex.height; uv[0] = nodeIndex % textureWidth / (textureWidth - 1); uv[1] = Math.floor(nodeIndex / textureWidth) / (textureHeight - 1); return uv; }; ForceAtlas2.prototype.getNodes = function () { return this._nodes; }; ForceAtlas2.prototype.getEdges = function () { return this._edges; }; ForceAtlas2.prototype.isFinished = function (maxSteps) { return this._frame > maxSteps; }; ForceAtlas2.prototype.getNodePosition = function (renderer, out) { if (!out) { out = new Float32Array(this._nodes.length * 2); } if (this._positionArr) { for (var i = 0; i < this._positionArr.length; i++) { out[i] = this._positionArr[i]; } } return out; }; ForceAtlas2.prototype._updateTexture = function (positionArr) { var pixels = this._positionTex.pixels; var offset = 0; for (var i = 0; i < positionArr.length;) { pixels[offset++] = positionArr[i++]; pixels[offset++] = positionArr[i++]; pixels[offset++] = 1; pixels[offset++] = 1; } this._positionTex.dirty(); }; ForceAtlas2.prototype.dispose = function (renderer) { this._disposed = true; this._worker = null; }; export default ForceAtlas2;