;(function(undefined) {
|
|
'use strict';
|
|
|
|
if (typeof sigma === 'undefined')
|
|
throw 'sigma is not declared';
|
|
|
|
/**
|
|
* Sigma ForceAtlas2.5 Supervisor
|
|
* ===============================
|
|
*
|
|
* Author: Guillaume Plique (Yomguithereal)
|
|
* Version: 0.1
|
|
*/
|
|
var _root = this;
|
|
|
|
/**
|
|
* Feature detection
|
|
* ------------------
|
|
*/
|
|
var webWorkers = 'Worker' in _root;
|
|
|
|
/**
|
|
* Supervisor Object
|
|
* ------------------
|
|
*/
|
|
function Supervisor(sigInst, options) {
|
|
var _this = this,
|
|
workerFn = sigInst.getForceAtlas2Worker &&
|
|
sigInst.getForceAtlas2Worker();
|
|
|
|
options = options || {};
|
|
|
|
// _root URL Polyfill
|
|
_root.URL = _root.URL || _root.webkitURL;
|
|
|
|
// Properties
|
|
this.sigInst = sigInst;
|
|
this.graph = this.sigInst.graph;
|
|
this.ppn = 10;
|
|
this.ppe = 3;
|
|
this.config = {};
|
|
this.shouldUseWorker =
|
|
options.worker === false ? false : true && webWorkers;
|
|
this.workerUrl = options.workerUrl;
|
|
|
|
// State
|
|
this.started = false;
|
|
this.running = false;
|
|
|
|
// Web worker or classic DOM events?
|
|
if (this.shouldUseWorker) {
|
|
if (!this.workerUrl) {
|
|
var blob = this.makeBlob(workerFn);
|
|
this.worker = new Worker(URL.createObjectURL(blob));
|
|
}
|
|
else {
|
|
this.worker = new Worker(this.workerUrl);
|
|
}
|
|
|
|
// Post Message Polyfill
|
|
this.worker.postMessage =
|
|
this.worker.webkitPostMessage || this.worker.postMessage;
|
|
}
|
|
else {
|
|
|
|
eval(workerFn);
|
|
}
|
|
|
|
// Worker message receiver
|
|
this.msgName = (this.worker) ? 'message' : 'newCoords';
|
|
this.listener = function(e) {
|
|
|
|
// Retrieving data
|
|
_this.nodesByteArray = new Float32Array(e.data.nodes);
|
|
|
|
// If ForceAtlas2 is running, we act accordingly
|
|
if (_this.running) {
|
|
|
|
// Applying layout
|
|
_this.applyLayoutChanges();
|
|
|
|
// Send data back to worker and loop
|
|
_this.sendByteArrayToWorker();
|
|
|
|
// Rendering graph
|
|
_this.sigInst.refresh();
|
|
}
|
|
};
|
|
|
|
(this.worker || document).addEventListener(this.msgName, this.listener);
|
|
|
|
// Filling byteArrays
|
|
this.graphToByteArrays();
|
|
|
|
// Binding on kill to properly terminate layout when parent is killed
|
|
sigInst.bind('kill', function() {
|
|
sigInst.killForceAtlas2();
|
|
});
|
|
}
|
|
|
|
Supervisor.prototype.makeBlob = function(workerFn) {
|
|
var blob;
|
|
|
|
try {
|
|
blob = new Blob([workerFn], {type: 'application/javascript'});
|
|
}
|
|
catch (e) {
|
|
_root.BlobBuilder = _root.BlobBuilder ||
|
|
_root.WebKitBlobBuilder ||
|
|
_root.MozBlobBuilder;
|
|
|
|
blob = new BlobBuilder();
|
|
blob.append(workerFn);
|
|
blob = blob.getBlob();
|
|
}
|
|
|
|
return blob;
|
|
};
|
|
|
|
Supervisor.prototype.graphToByteArrays = function() {
|
|
var nodes = this.graph.nodes(),
|
|
edges = this.graph.edges(),
|
|
nbytes = nodes.length * this.ppn,
|
|
ebytes = edges.length * this.ppe,
|
|
nIndex = {},
|
|
i,
|
|
j,
|
|
l;
|
|
|
|
// Allocating Byte arrays with correct nb of bytes
|
|
this.nodesByteArray = new Float32Array(nbytes);
|
|
this.edgesByteArray = new Float32Array(ebytes);
|
|
|
|
// Iterate through nodes
|
|
for (i = j = 0, l = nodes.length; i < l; i++) {
|
|
|
|
// Populating index
|
|
nIndex[nodes[i].id] = j;
|
|
|
|
// Populating byte array
|
|
this.nodesByteArray[j] = nodes[i].x;
|
|
this.nodesByteArray[j + 1] = nodes[i].y;
|
|
this.nodesByteArray[j + 2] = 0;
|
|
this.nodesByteArray[j + 3] = 0;
|
|
this.nodesByteArray[j + 4] = 0;
|
|
this.nodesByteArray[j + 5] = 0;
|
|
this.nodesByteArray[j + 6] = 1 + this.graph.degree(nodes[i].id);
|
|
this.nodesByteArray[j + 7] = 1;
|
|
this.nodesByteArray[j + 8] = nodes[i].size;
|
|
this.nodesByteArray[j + 9] = 0;
|
|
j += this.ppn;
|
|
}
|
|
|
|
// Iterate through edges
|
|
for (i = j = 0, l = edges.length; i < l; i++) {
|
|
this.edgesByteArray[j] = nIndex[edges[i].source];
|
|
this.edgesByteArray[j + 1] = nIndex[edges[i].target];
|
|
this.edgesByteArray[j + 2] = edges[i].weight || 0;
|
|
j += this.ppe;
|
|
}
|
|
};
|
|
|
|
// TODO: make a better send function
|
|
Supervisor.prototype.applyLayoutChanges = function() {
|
|
var nodes = this.graph.nodes(),
|
|
j = 0,
|
|
realIndex;
|
|
|
|
// Moving nodes
|
|
for (var i = 0, l = this.nodesByteArray.length; i < l; i += this.ppn) {
|
|
nodes[j].x = this.nodesByteArray[i];
|
|
nodes[j].y = this.nodesByteArray[i + 1];
|
|
j++;
|
|
}
|
|
};
|
|
|
|
Supervisor.prototype.sendByteArrayToWorker = function(action) {
|
|
var content = {
|
|
action: action || 'loop',
|
|
nodes: this.nodesByteArray.buffer
|
|
};
|
|
|
|
var buffers = [this.nodesByteArray.buffer];
|
|
|
|
if (action === 'start') {
|
|
content.config = this.config || {};
|
|
content.edges = this.edgesByteArray.buffer;
|
|
buffers.push(this.edgesByteArray.buffer);
|
|
}
|
|
|
|
if (this.shouldUseWorker)
|
|
this.worker.postMessage(content, buffers);
|
|
else
|
|
_root.postMessage(content, '*');
|
|
};
|
|
|
|
Supervisor.prototype.start = function() {
|
|
if (this.running)
|
|
return;
|
|
|
|
this.running = true;
|
|
|
|
// Do not refresh edgequadtree during layout:
|
|
var k,
|
|
c;
|
|
for (k in this.sigInst.cameras) {
|
|
c = this.sigInst.cameras[k];
|
|
c.edgequadtree._enabled = false;
|
|
}
|
|
|
|
if (!this.started) {
|
|
|
|
// Sending init message to worker
|
|
this.sendByteArrayToWorker('start');
|
|
this.started = true;
|
|
}
|
|
else {
|
|
this.sendByteArrayToWorker();
|
|
}
|
|
};
|
|
|
|
Supervisor.prototype.stop = function() {
|
|
if (!this.running)
|
|
return;
|
|
|
|
// Allow to refresh edgequadtree:
|
|
var k,
|
|
c,
|
|
bounds;
|
|
for (k in this.sigInst.cameras) {
|
|
c = this.sigInst.cameras[k];
|
|
c.edgequadtree._enabled = true;
|
|
|
|
// Find graph boundaries:
|
|
bounds = sigma.utils.getBoundaries(
|
|
this.graph,
|
|
c.readPrefix
|
|
);
|
|
|
|
// Refresh edgequadtree:
|
|
if (c.settings('drawEdges') && c.settings('enableEdgeHovering'))
|
|
c.edgequadtree.index(this.sigInst.graph, {
|
|
prefix: c.readPrefix,
|
|
bounds: {
|
|
x: bounds.minX,
|
|
y: bounds.minY,
|
|
width: bounds.maxX - bounds.minX,
|
|
height: bounds.maxY - bounds.minY
|
|
}
|
|
});
|
|
}
|
|
|
|
this.running = false;
|
|
};
|
|
|
|
Supervisor.prototype.killWorker = function() {
|
|
if (this.worker) {
|
|
this.worker.terminate();
|
|
}
|
|
else {
|
|
_root.postMessage({action: 'kill'}, '*');
|
|
document.removeEventListener(this.msgName, this.listener);
|
|
}
|
|
};
|
|
|
|
Supervisor.prototype.configure = function(config) {
|
|
|
|
// Setting configuration
|
|
this.config = config;
|
|
|
|
if (!this.started)
|
|
return;
|
|
|
|
var data = {action: 'config', config: this.config};
|
|
|
|
if (this.shouldUseWorker)
|
|
this.worker.postMessage(data);
|
|
else
|
|
_root.postMessage(data, '*');
|
|
};
|
|
|
|
/**
|
|
* Interface
|
|
* ----------
|
|
*/
|
|
sigma.prototype.startForceAtlas2 = function(config) {
|
|
|
|
// Create supervisor if undefined
|
|
if (!this.supervisor)
|
|
this.supervisor = new Supervisor(this, config);
|
|
|
|
// Configuration provided?
|
|
if (config)
|
|
this.supervisor.configure(config);
|
|
|
|
// Start algorithm
|
|
this.supervisor.start();
|
|
|
|
return this;
|
|
};
|
|
|
|
sigma.prototype.stopForceAtlas2 = function() {
|
|
if (!this.supervisor)
|
|
return this;
|
|
|
|
// Pause algorithm
|
|
this.supervisor.stop();
|
|
|
|
return this;
|
|
};
|
|
|
|
sigma.prototype.killForceAtlas2 = function() {
|
|
if (!this.supervisor)
|
|
return this;
|
|
|
|
// Stop Algorithm
|
|
this.supervisor.stop();
|
|
|
|
// Kill Worker
|
|
this.supervisor.killWorker();
|
|
|
|
// Kill supervisor
|
|
this.supervisor = null;
|
|
|
|
return this;
|
|
};
|
|
|
|
sigma.prototype.configForceAtlas2 = function(config) {
|
|
if (!this.supervisor)
|
|
this.supervisor = new Supervisor(this, config);
|
|
|
|
this.supervisor.configure(config);
|
|
|
|
return this;
|
|
};
|
|
|
|
sigma.prototype.isForceAtlas2Running = function(config) {
|
|
return !!this.supervisor && this.supervisor.running;
|
|
};
|
|
}).call(this);
|