ccnetviz
Version:
[](https://travis-ci.org/HelikarLab/ccNetViz) [](https://www.gnu.org/licenses/gpl-3.0) [![semantic-releas
1,880 lines (1,635 loc) • 352 kB
JavaScript
;(function(undefined) {
'use strict';
var __instances = {};
/**
* This is the sigma instances constructor. One instance of sigma represent
* one graph. It is possible to represent this grapĥ with several renderers
* at the same time. By default, the default renderer (WebGL + Canvas
* polyfill) will be used as the only renderer, with the container specified
* in the configuration.
*
* @param {?*} conf The configuration of the instance. There are a lot of
* different recognized forms to instantiate sigma, check
* example files, documentation in this file and unit
* tests to know more.
* @return {sigma} The fresh new sigma instance.
*
* Instanciating sigma:
* ********************
* If no parameter is given to the constructor, the instance will be created
* without any renderer or camera. It will just instantiate the graph, and
* other modules will have to be instantiated through the public methods,
* like "addRenderer" etc:
*
* > s0 = new sigma();
* > s0.addRenderer({
* > type: 'canvas',
* > container: 'my-container-id'
* > });
*
* In most of the cases, sigma will simply be used with the default renderer.
* Then, since the only required parameter is the DOM container, there are
* some simpler way to call the constructor. The four following calls do the
* exact same things:
*
* > s1 = new sigma('my-container-id');
* > s2 = new sigma(document.getElementById('my-container-id'));
* > s3 = new sigma({
* > container: document.getElementById('my-container-id')
* > });
* > s4 = new sigma({
* > renderers: [{
* > container: document.getElementById('my-container-id')
* > }]
* > });
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters, when calling the
* constructor with to top level configuration object (fourth case in the
* previous examples):
*
* {?string} id The id of the instance. It will be generated
* automatically if not specified.
* {?array} renderers An array containing objects describing renderers.
* {?object} graph An object containing an array of nodes and an array
* of edges, to avoid having to add them by hand later.
* {?object} settings An object containing instance specific settings that
* will override the default ones defined in the object
* sigma.settings.
*/
var sigma = function(conf) {
// Local variables:
// ****************
var i,
l,
a,
c,
o,
id;
sigma.classes.dispatcher.extend(this);
// Private attributes:
// *******************
var _self = this,
_conf = conf || {};
// Little shortcut:
// ****************
// The configuration is supposed to have a list of the configuration
// objects for each renderer.
// - If there are no configuration at all, then nothing is done.
// - If there are no renderer list, the given configuration object will be
// considered as describing the first and only renderer.
// - If there are no renderer list nor "container" object, it will be
// considered as the container itself (a DOM element).
// - If the argument passed to sigma() is a string, it will be considered
// as the ID of the DOM container.
if (
typeof _conf === 'string' ||
_conf instanceof HTMLElement
)
_conf = {
renderers: [_conf]
};
else if (Object.prototype.toString.call(_conf) === '[object Array]')
_conf = {
renderers: _conf
};
// Also check "renderer" and "container" keys:
o = _conf.renderers || _conf.renderer || _conf.container;
if (!_conf.renderers || _conf.renderers.length === 0)
if (
typeof o === 'string' ||
o instanceof HTMLElement ||
(typeof o === 'object' && 'container' in o)
)
_conf.renderers = [o];
// Recense the instance:
if (_conf.id) {
if (__instances[_conf.id])
throw 'sigma: Instance "' + _conf.id + '" already exists.';
Object.defineProperty(this, 'id', {
value: _conf.id
});
} else {
id = 0;
while (__instances[id])
id++;
Object.defineProperty(this, 'id', {
value: '' + id
});
}
__instances[this.id] = this;
// Initialize settings function:
this.settings = new sigma.classes.configurable(
sigma.settings,
_conf.settings || {}
);
// Initialize locked attributes:
Object.defineProperty(this, 'graph', {
value: new sigma.classes.graph(this.settings),
configurable: true
});
Object.defineProperty(this, 'middlewares', {
value: [],
configurable: true
});
Object.defineProperty(this, 'cameras', {
value: {},
configurable: true
});
Object.defineProperty(this, 'renderers', {
value: {},
configurable: true
});
Object.defineProperty(this, 'renderersPerCamera', {
value: {},
configurable: true
});
Object.defineProperty(this, 'cameraFrames', {
value: {},
configurable: true
});
Object.defineProperty(this, 'camera', {
get: function() {
return this.cameras[0];
}
});
Object.defineProperty(this, 'events', {
value: [
'click',
'rightClick',
'clickStage',
'doubleClickStage',
'rightClickStage',
'clickNode',
'clickNodes',
'doubleClickNode',
'doubleClickNodes',
'rightClickNode',
'rightClickNodes',
'overNode',
'overNodes',
'outNode',
'outNodes',
'downNode',
'downNodes',
'upNode',
'upNodes'
],
configurable: true
});
// Add a custom handler, to redispatch events from renderers:
this._handler = (function(e) {
var k,
data = {};
for (k in e.data)
data[k] = e.data[k];
data.renderer = e.target;
this.dispatchEvent(e.type, data);
}).bind(this);
// Initialize renderers:
a = _conf.renderers || [];
for (i = 0, l = a.length; i < l; i++)
this.addRenderer(a[i]);
// Initialize middlewares:
a = _conf.middlewares || [];
for (i = 0, l = a.length; i < l; i++)
this.middlewares.push(
typeof a[i] === 'string' ?
sigma.middlewares[a[i]] :
a[i]
);
// Check if there is already a graph to fill in:
if (typeof _conf.graph === 'object' && _conf.graph) {
this.graph.read(_conf.graph);
// If a graph is given to the to the instance, the "refresh" method is
// directly called:
this.refresh();
}
// Deal with resize:
window.addEventListener('resize', function() {
if (_self.settings)
_self.refresh();
});
};
/**
* This methods will instantiate and reference a new camera. If no id is
* specified, then an automatic id will be generated.
*
* @param {?string} id Eventually the camera id.
* @return {sigma.classes.camera} The fresh new camera instance.
*/
sigma.prototype.addCamera = function(id) {
var self = this,
camera;
if (!arguments.length) {
id = 0;
while (this.cameras['' + id])
id++;
id = '' + id;
}
if (this.cameras[id])
throw 'sigma.addCamera: The camera "' + id + '" already exists.';
camera = new sigma.classes.camera(id, this.graph, this.settings);
this.cameras[id] = camera;
// Add a quadtree to the camera:
camera.quadtree = new sigma.classes.quad();
// Add an edgequadtree to the camera:
if (sigma.classes.edgequad !== undefined) {
camera.edgequadtree = new sigma.classes.edgequad();
}
camera.bind('coordinatesUpdated', function(e) {
self.renderCamera(camera, camera.isAnimated);
});
this.renderersPerCamera[id] = [];
return camera;
};
/**
* This method kills a camera, and every renderer attached to it.
*
* @param {string|camera} v The camera to kill or its ID.
* @return {sigma} Returns the instance.
*/
sigma.prototype.killCamera = function(v) {
v = typeof v === 'string' ? this.cameras[v] : v;
if (!v)
throw 'sigma.killCamera: The camera is undefined.';
var i,
l,
a = this.renderersPerCamera[v.id];
for (l = a.length, i = l - 1; i >= 0; i--)
this.killRenderer(a[i]);
delete this.renderersPerCamera[v.id];
delete this.cameraFrames[v.id];
delete this.cameras[v.id];
if (v.kill)
v.kill();
return this;
};
/**
* This methods will instantiate and reference a new renderer. The "type"
* argument can be the constructor or its name in the "sigma.renderers"
* package. If no type is specified, then "sigma.renderers.def" will be used.
* If no id is specified, then an automatic id will be generated.
*
* @param {?object} options Eventually some options to give to the renderer
* constructor.
* @return {renderer} The fresh new renderer instance.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the "options"
* object:
*
* {?string} id Eventually the renderer id.
* {?(function|string)} type Eventually the renderer constructor or its
* name in the "sigma.renderers" package.
* {?(camera|string)} camera Eventually the renderer camera or its
* id.
*/
sigma.prototype.addRenderer = function(options) {
var id,
fn,
camera,
renderer,
o = options || {};
// Polymorphism:
if (typeof o === 'string')
o = {
container: document.getElementById(o)
};
else if (o instanceof HTMLElement)
o = {
container: o
};
// If the container still is a string, we get it by id
if (typeof o.container === 'string')
o.container = document.getElementById(o.container);
// Reference the new renderer:
if (!('id' in o)) {
id = 0;
while (this.renderers['' + id])
id++;
id = '' + id;
} else
id = o.id;
if (this.renderers[id])
throw 'sigma.addRenderer: The renderer "' + id + '" already exists.';
// Find the good constructor:
fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type];
fn = fn || sigma.renderers.def;
// Find the good camera:
camera = 'camera' in o ?
(
o.camera instanceof sigma.classes.camera ?
o.camera :
this.cameras[o.camera] || this.addCamera(o.camera)
) :
this.addCamera();
if (this.cameras[camera.id] !== camera)
throw 'sigma.addRenderer: The camera is not properly referenced.';
// Instantiate:
renderer = new fn(this.graph, camera, this.settings, o);
this.renderers[id] = renderer;
Object.defineProperty(renderer, 'id', {
value: id
});
// Bind events:
if (renderer.bind)
renderer.bind(
[
'click',
'rightClick',
'clickStage',
'doubleClickStage',
'rightClickStage',
'clickNode',
'clickNodes',
'clickEdge',
'clickEdges',
'doubleClickNode',
'doubleClickNodes',
'doubleClickEdge',
'doubleClickEdges',
'rightClickNode',
'rightClickNodes',
'rightClickEdge',
'rightClickEdges',
'overNode',
'overNodes',
'overEdge',
'overEdges',
'outNode',
'outNodes',
'outEdge',
'outEdges',
'downNode',
'downNodes',
'downEdge',
'downEdges',
'upNode',
'upNodes',
'upEdge',
'upEdges'
],
this._handler
);
// Reference the renderer by its camera:
this.renderersPerCamera[camera.id].push(renderer);
return renderer;
};
/**
* This method kills a renderer.
*
* @param {string|renderer} v The renderer to kill or its ID.
* @return {sigma} Returns the instance.
*/
sigma.prototype.killRenderer = function(v) {
v = typeof v === 'string' ? this.renderers[v] : v;
if (!v)
throw 'sigma.killRenderer: The renderer is undefined.';
var a = this.renderersPerCamera[v.camera.id],
i = a.indexOf(v);
if (i >= 0)
a.splice(i, 1);
if (v.kill)
v.kill();
delete this.renderers[v.id];
return this;
};
/**
* This method calls the "render" method of each renderer, with the same
* arguments than the "render" method, but will also check if the renderer
* has a "process" method, and call it if it exists.
*
* It is useful for quadtrees or WebGL processing, for instance.
*
* @param {?object} options Eventually some options to give to the refresh
* method.
* @return {sigma} Returns the instance itself.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the "options"
* object:
*
* {?boolean} skipIndexation A flag specifying wether or not the refresh
* function should reindex the graph in the
* quadtrees or not (default: false).
*/
sigma.prototype.refresh = function(options) {
var i,
l,
k,
a,
c,
bounds,
prefix = 0;
options = options || {};
// Call each middleware:
a = this.middlewares || [];
for (i = 0, l = a.length; i < l; i++)
a[i].call(
this,
(i === 0) ? '' : 'tmp' + prefix + ':',
(i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':')
);
// Then, for each camera, call the "rescale" middleware, unless the
// settings specify not to:
for (k in this.cameras) {
c = this.cameras[k];
if (
c.settings('autoRescale') &&
this.renderersPerCamera[c.id] &&
this.renderersPerCamera[c.id].length
)
sigma.middlewares.rescale.call(
this,
a.length ? 'ready:' : '',
c.readPrefix,
{
width: this.renderersPerCamera[c.id][0].width,
height: this.renderersPerCamera[c.id][0].height
}
);
else
sigma.middlewares.copy.call(
this,
a.length ? 'ready:' : '',
c.readPrefix
);
if (!options.skipIndexation) {
// Find graph boundaries:
bounds = sigma.utils.getBoundaries(
this.graph,
c.readPrefix
);
// Refresh quadtree:
c.quadtree.index(this.graph.nodes(), {
prefix: c.readPrefix,
bounds: {
x: bounds.minX,
y: bounds.minY,
width: bounds.maxX - bounds.minX,
height: bounds.maxY - bounds.minY
}
});
// Refresh edgequadtree:
if (
c.edgequadtree !== undefined &&
c.settings('drawEdges') &&
c.settings('enableEdgeHovering')
) {
c.edgequadtree.index(this.graph, {
prefix: c.readPrefix,
bounds: {
x: bounds.minX,
y: bounds.minY,
width: bounds.maxX - bounds.minX,
height: bounds.maxY - bounds.minY
}
});
}
}
}
// Call each renderer:
a = Object.keys(this.renderers);
for (i = 0, l = a.length; i < l; i++)
if (this.renderers[a[i]].process) {
if (this.settings('skipErrors'))
try {
this.renderers[a[i]].process();
} catch (e) {
console.log(
'Warning: The renderer "' + a[i] + '" crashed on ".process()"'
);
}
else
this.renderers[a[i]].process();
}
this.render();
return this;
};
/**
* This method calls the "render" method of each renderer.
*
* @return {sigma} Returns the instance itself.
*/
sigma.prototype.render = function() {
var i,
l,
a,
prefix = 0;
// Call each renderer:
a = Object.keys(this.renderers);
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
this.renderers[a[i]].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' + a[i] + '" crashed on ".render()"'
);
}
else
this.renderers[a[i]].render();
return this;
};
/**
* This method calls the "render" method of each renderer that is bound to
* the specified camera. To improve the performances, if this method is
* called too often, the number of effective renderings is limitated to one
* per frame, unless you are using the "force" flag.
*
* @param {sigma.classes.camera} camera The camera to render.
* @param {?boolean} force If true, will render the camera
* directly.
* @return {sigma} Returns the instance itself.
*/
sigma.prototype.renderCamera = function(camera, force) {
var i,
l,
a,
self = this;
if (force) {
a = this.renderersPerCamera[camera.id];
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
a[i].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' + a[i].id + '" crashed on ".render()"'
);
}
else
a[i].render();
} else {
if (!this.cameraFrames[camera.id]) {
a = this.renderersPerCamera[camera.id];
for (i = 0, l = a.length; i < l; i++)
if (this.settings('skipErrors'))
try {
a[i].render();
} catch (e) {
if (this.settings('verbose'))
console.log(
'Warning: The renderer "' +
a[i].id +
'" crashed on ".render()"'
);
}
else
a[i].render();
this.cameraFrames[camera.id] = requestAnimationFrame(function() {
delete self.cameraFrames[camera.id];
});
}
}
return this;
};
/**
* This method calls the "kill" method of each module and destroys any
* reference from the instance.
*/
sigma.prototype.kill = function() {
var k;
// Dispatching event
this.dispatchEvent('kill');
// Kill graph:
this.graph.kill();
// Kill middlewares:
delete this.middlewares;
// Kill each renderer:
for (k in this.renderers)
this.killRenderer(this.renderers[k]);
// Kill each camera:
for (k in this.cameras)
this.killCamera(this.cameras[k]);
delete this.renderers;
delete this.cameras;
// Kill everything else:
for (k in this)
if (this.hasOwnProperty(k))
delete this[k];
delete __instances[this.id];
};
/**
* Returns a clone of the instances object or a specific running instance.
*
* @param {?string} id Eventually an instance ID.
* @return {object} The related instance or a clone of the instances
* object.
*/
sigma.instances = function(id) {
return arguments.length ?
__instances[id] :
sigma.utils.extend({}, __instances);
};
/**
* The current version of sigma:
*/
sigma.version = '1.2.0';
/**
* EXPORT:
* *******
*/
if (typeof this.sigma !== 'undefined')
throw 'An object called sigma is already in the global scope.';
this.sigma = sigma;
}).call(this);
/**
* conrad.js is a tiny JavaScript jobs scheduler,
*
* Version: 0.1.0
* Sources: http://github.com/jacomyal/conrad.js
* Doc: http://github.com/jacomyal/conrad.js#readme
*
* License:
* --------
* Copyright © 2013 Alexis Jacomy, Sciences-Po médialab
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* The Software is provided "as is", without warranty of any kind, express or
* implied, including but not limited to the warranties of merchantability,
* fitness for a particular purpose and noninfringement. In no event shall the
* authors or copyright holders be liable for any claim, damages or other
* liability, whether in an action of contract, tort or otherwise, arising
* from, out of or in connection with the software or the use or other dealings
* in the Software.
*/
(function(global) {
'use strict';
// Check that conrad.js has not been loaded yet:
if (global.conrad)
throw new Error('conrad already exists');
/**
* PRIVATE VARIABLES:
* ******************
*/
/**
* A flag indicating whether conrad is running or not.
*
* @type {Number}
*/
var _lastFrameTime;
/**
* A flag indicating whether conrad is running or not.
*
* @type {Boolean}
*/
var _isRunning = false;
/**
* The hash of registered jobs. Each job must at least have a unique ID
* under the key "id" and a function under the key "job". This hash
* contains each running job and each waiting job.
*
* @type {Object}
*/
var _jobs = {};
/**
* The hash of currently running jobs.
*
* @type {Object}
*/
var _runningJobs = {};
/**
* The array of currently running jobs, sorted by priority.
*
* @type {Array}
*/
var _sortedByPriorityJobs = [];
/**
* The array of currently waiting jobs.
*
* @type {Object}
*/
var _waitingJobs = {};
/**
* The array of finished jobs. They are stored in an array, since two jobs
* with the same "id" can happen at two different times.
*
* @type {Array}
*/
var _doneJobs = [];
/**
* A dirty flag to keep conrad from starting: Indeed, when addJob() is called
* with several jobs, conrad must be started only at the end. This flag keeps
* me from duplicating the code that effectively adds a job.
*
* @type {Boolean}
*/
var _noStart = false;
/**
* An hash containing some global settings about how conrad.js should
* behave.
*
* @type {Object}
*/
var _parameters = {
frameDuration: 20,
history: true
};
/**
* This object contains every handlers bound to conrad events. It does not
* requirea any DOM implementation, since the events are all JavaScript.
*
* @type {Object}
*/
var _handlers = Object.create(null);
/**
* PRIVATE FUNCTIONS:
* ******************
*/
/**
* Will execute the handler everytime that the indicated event (or the
* indicated events) will be triggered.
*
* @param {string|array|object} events The name of the event (or the events
* separated by spaces).
* @param {function(Object)} handler The handler to bind.
* @return {Object} Returns conrad.
*/
function _bind(events, handler) {
var i,
i_end,
event,
eArray;
if (!arguments.length)
return;
else if (
arguments.length === 1 &&
Object(arguments[0]) === arguments[0]
)
for (events in arguments[0])
_bind(events, arguments[0][events]);
else if (arguments.length > 1) {
eArray =
Array.isArray(events) ?
events :
events.split(/ /);
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
event = eArray[i];
if (!_handlers[event])
_handlers[event] = [];
// Using an object instead of directly the handler will make possible
// later to add flags
_handlers[event].push({
handler: handler
});
}
}
}
/**
* Removes the handler from a specified event (or specified events).
*
* @param {?string} events The name of the event (or the events
* separated by spaces). If undefined,
* then all handlers are removed.
* @param {?function(Object)} handler The handler to unbind. If undefined,
* each handler bound to the event or the
* events will be removed.
* @return {Object} Returns conrad.
*/
function _unbind(events, handler) {
var i,
i_end,
j,
j_end,
a,
event,
eArray = Array.isArray(events) ?
events :
events.split(/ /);
if (!arguments.length)
_handlers = Object.create(null);
else if (handler) {
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
event = eArray[i];
if (_handlers[event]) {
a = [];
for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1)
if (_handlers[event][j].handler !== handler)
a.push(_handlers[event][j]);
_handlers[event] = a;
}
if (_handlers[event] && _handlers[event].length === 0)
delete _handlers[event];
}
} else
for (i = 0, i_end = eArray.length; i !== i_end; i += 1)
delete _handlers[eArray[i]];
}
/**
* Executes each handler bound to the event.
*
* @param {string} events The name of the event (or the events separated
* by spaces).
* @param {?Object} data The content of the event (optional).
* @return {Object} Returns conrad.
*/
function _dispatch(events, data) {
var i,
j,
i_end,
j_end,
event,
eventName,
eArray = Array.isArray(events) ?
events :
events.split(/ /);
data = data === undefined ? {} : data;
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
eventName = eArray[i];
if (_handlers[eventName]) {
event = {
type: eventName,
data: data || {}
};
for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1)
try {
_handlers[eventName][j].handler(event);
} catch (e) {}
}
}
}
/**
* Executes the most prioritary job once, and deals with filling the stats
* (done, time, averageTime, currentTime, etc...).
*
* @return {?Object} Returns the job object if it has to be killed, null else.
*/
function _executeFirstJob() {
var i,
l,
test,
kill,
pushed = false,
time = __dateNow(),
job = _sortedByPriorityJobs.shift();
// Execute the job and look at the result:
test = job.job();
// Deal with stats:
time = __dateNow() - time;
job.done++;
job.time += time;
job.currentTime += time;
job.weightTime = job.currentTime / (job.weight || 1);
job.averageTime = job.time / job.done;
// Check if the job has to be killed:
kill = job.count ? (job.count <= job.done) : !test;
// Reset priorities:
if (!kill) {
for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++)
if (_sortedByPriorityJobs[i].weightTime > job.weightTime) {
_sortedByPriorityJobs.splice(i, 0, job);
pushed = true;
break;
}
if (!pushed)
_sortedByPriorityJobs.push(job);
}
return kill ? job : null;
}
/**
* Activates a job, by adding it to the _runningJobs object and the
* _sortedByPriorityJobs array. It also initializes its currentTime value.
*
* @param {Object} job The job to activate.
*/
function _activateJob(job) {
var l = _sortedByPriorityJobs.length;
// Add the job to the running jobs:
_runningJobs[job.id] = job;
job.status = 'running';
// Add the job to the priorities:
if (l) {
job.weightTime = _sortedByPriorityJobs[l - 1].weightTime;
job.currentTime = job.weightTime * (job.weight || 1);
}
// Initialize the job and dispatch:
job.startTime = __dateNow();
_dispatch('jobStarted', __clone(job));
_sortedByPriorityJobs.push(job);
}
/**
* The main loop of conrad.js:
* . It executes job such that they all occupate the same processing time.
* . It stops jobs that do not need to be executed anymore.
* . It triggers callbacks when it is relevant.
* . It starts waiting jobs when they need to be started.
* . It injects frames to keep a constant frapes per second ratio.
* . It stops itself when there are no more jobs to execute.
*/
function _loop() {
var k,
o,
l,
job,
time,
deadJob;
// Deal with the newly added jobs (the _jobs object):
for (k in _jobs) {
job = _jobs[k];
if (job.after)
_waitingJobs[k] = job;
else
_activateJob(job);
delete _jobs[k];
}
// Set the _isRunning flag to false if there are no running job:
_isRunning = !!_sortedByPriorityJobs.length;
// Deal with the running jobs (the _runningJobs object):
while (
_sortedByPriorityJobs.length &&
__dateNow() - _lastFrameTime < _parameters.frameDuration
) {
deadJob = _executeFirstJob();
// Deal with the case where the job has ended:
if (deadJob) {
_killJob(deadJob.id);
// Check for waiting jobs:
for (k in _waitingJobs)
if (_waitingJobs[k].after === deadJob.id) {
_activateJob(_waitingJobs[k]);
delete _waitingJobs[k];
}
}
}
// Check if conrad still has jobs to deal with, and kill it if not:
if (_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('enterFrame');
setTimeout(_loop, 0);
} else
_dispatch('stop');
}
/**
* Adds one or more jobs, and starts the loop if no job was running before. A
* job is at least a unique string "id" and a function, and there are some
* parameters that you can specify for each job to modify the way conrad will
* execute it. If a job is added with the "id" of another job that is waiting
* or still running, an error will be thrown.
*
* When a job is added, it is referenced in the _jobs object, by its id.
* Then, if it has to be executed right now, it will be also referenced in
* the _runningJobs object. If it has to wait, then it will be added into the
* _waitingJobs object, until it can start.
*
* Keep reading this documentation to see how to call this method.
*
* @return {Object} Returns conrad.
*
* Adding one job:
* ***************
* Basically, a job is defined by its string id and a function (the job). It
* is also possible to add some parameters:
*
* > conrad.addJob('myJobId', myJobFunction);
* > conrad.addJob('myJobId', {
* > job: myJobFunction,
* > someParameter: someValue
* > });
* > conrad.addJob({
* > id: 'myJobId',
* > job: myJobFunction,
* > someParameter: someValue
* > });
*
* Adding several jobs:
* ********************
* When adding several jobs at the same time, it is possible to specify
* parameters for each one individually or for all:
*
* > conrad.addJob([
* > {
* > id: 'myJobId1',
* > job: myJobFunction1,
* > someParameter1: someValue1
* > },
* > {
* > id: 'myJobId2',
* > job: myJobFunction2,
* > someParameter2: someValue2
* > }
* > ], {
* > someCommonParameter: someCommonValue
* > });
* > conrad.addJob({
* > myJobId1: {,
* > job: myJobFunction1,
* > someParameter1: someValue1
* > },
* > myJobId2: {,
* > job: myJobFunction2,
* > someParameter2: someValue2
* > }
* > }, {
* > someCommonParameter: someCommonValue
* > });
* > conrad.addJob({
* > myJobId1: myJobFunction1,
* > myJobId2: myJobFunction2
* > }, {
* > someCommonParameter: someCommonValue
* > });
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters:
*
* {?Function} end A callback to execute when the job is ended. It is
* not executed if the job is killed instead of ended
* "naturally".
* {?Integer} count The number of time the job has to be executed.
* {?Number} weight If specified, the job will be executed as it was
* added "weight" times.
* {?String} after The id of another job (eventually not added yet).
* If specified, this job will start only when the
* specified "after" job is ended.
*/
function _addJob(v1, v2) {
var i,
l,
o;
// Array of jobs:
if (Array.isArray(v1)) {
// Keep conrad to start until the last job is added:
_noStart = true;
for (i = 0, l = v1.length; i < l; i++)
_addJob(v1[i].id, __extend(v1[i], v2));
_noStart = false;
if (!_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
} else if (typeof v1 === 'object') {
// One job (object):
if (typeof v1.id === 'string')
_addJob(v1.id, v1);
// Hash of jobs:
else {
// Keep conrad to start until the last job is added:
_noStart = true;
for (i in v1)
if (typeof v1[i] === 'function')
_addJob(i, __extend({
job: v1[i]
}, v2));
else
_addJob(i, __extend(v1[i], v2));
_noStart = false;
if (!_isRunning) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
}
// One job (string, *):
} else if (typeof v1 === 'string') {
if (_hasJob(v1))
throw new Error(
'[conrad.addJob] Job with id "' + v1 + '" already exists.'
);
// One job (string, function):
if (typeof v2 === 'function') {
o = {
id: v1,
done: 0,
time: 0,
status: 'waiting',
currentTime: 0,
averageTime: 0,
weightTime: 0,
job: v2
};
// One job (string, object):
} else if (typeof v2 === 'object') {
o = __extend(
{
id: v1,
done: 0,
time: 0,
status: 'waiting',
currentTime: 0,
averageTime: 0,
weightTime: 0
},
v2
);
// If none of those cases, throw an error:
} else
throw new Error('[conrad.addJob] Wrong arguments.');
// Effectively add the job:
_jobs[v1] = o;
_dispatch('jobAdded', __clone(o));
// Check if the loop has to be started:
if (!_isRunning && !_noStart) {
// Update the _lastFrameTime:
_lastFrameTime = __dateNow();
_dispatch('start');
_loop();
}
// If none of those cases, throw an error:
} else
throw new Error('[conrad.addJob] Wrong arguments.');
return this;
}
/**
* Kills one or more jobs, indicated by their ids. It is only possible to
* kill running jobs or waiting jobs. If you try to kill a job that does not
* exist or that is already killed, a warning will be thrown.
*
* @param {Array|String} v1 A string job id or an array of job ids.
* @return {Object} Returns conrad.
*/
function _killJob(v1) {
var i,
l,
k,
a,
job,
found = false;
// Array of job ids:
if (Array.isArray(v1))
for (i = 0, l = v1.length; i < l; i++)
_killJob(v1[i]);
// One job's id:
else if (typeof v1 === 'string') {
a = [_runningJobs, _waitingJobs, _jobs];
// Remove the job from the hashes:
for (i = 0, l = a.length; i < l; i++)
if (v1 in a[i]) {
job = a[i][v1];
if (_parameters.history) {
job.status = 'done';
_doneJobs.push(job);
}
_dispatch('jobEnded', __clone(job));
delete a[i][v1];
if (typeof job.end === 'function')
job.end();
found = true;
}
// Remove the priorities array:
a = _sortedByPriorityJobs;
for (i = 0, l = a.length; i < l; i++)
if (a[i].id === v1) {
a.splice(i, 1);
break;
}
if (!found)
throw new Error('[conrad.killJob] Job "' + v1 + '" not found.');
// If none of those cases, throw an error:
} else
throw new Error('[conrad.killJob] Wrong arguments.');
return this;
}
/**
* Kills every running, waiting, and just added jobs.
*
* @return {Object} Returns conrad.
*/
function _killAll() {
var k,
jobs = __extend(_jobs, _runningJobs, _waitingJobs);
// Take every jobs and push them into the _doneJobs object:
if (_parameters.history)
for (k in jobs) {
jobs[k].status = 'done';
_doneJobs.push(jobs[k]);
if (typeof jobs[k].end === 'function')
jobs[k].end();
}
// Reinitialize the different jobs lists:
_jobs = {};
_waitingJobs = {};
_runningJobs = {};
_sortedByPriorityJobs = [];
// In case some jobs are added right after the kill:
_isRunning = false;
return this;
}
/**
* Returns true if a job with the specified id is currently running or
* waiting, and false else.
*
* @param {String} id The id of the job.
* @return {?Object} Returns the job object if it exists.
*/
function _hasJob(id) {
var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id];
return job ? __extend(job) : null;
}
/**
* This method will set the setting specified by "v1" to the value specified
* by "v2" if both are given, and else return the current value of the
* settings "v1".
*
* @param {String} v1 The name of the property.
* @param {?*} v2 Eventually, a value to set to the specified
* property.
* @return {Object|*} Returns the specified settings value if "v2" is not
* given, and conrad else.
*/
function _settings(v1, v2) {
var o;
if (typeof a1 === 'string' && arguments.length === 1)
return _parameters[a1];
else {
o = (typeof a1 === 'object' && arguments.length === 1) ?
a1 || {} :
{};
if (typeof a1 === 'string')
o[a1] = a2;
for (var k in o)
if (o[k] !== undefined)
_parameters[k] = o[k];
else
delete _parameters[k];
return this;
}
}
/**
* Returns true if conrad is currently running, and false else.
*
* @return {Boolean} Returns _isRunning.
*/
function _getIsRunning() {
return _isRunning;
}
/**
* Unreference every job that is stored in the _doneJobs object. It will
* not be possible anymore to get stats about these jobs, but it will release
* the memory.
*
* @return {Object} Returns conrad.
*/
function _clearHistory() {
_doneJobs = [];
return this;
}
/**
* Returns a snapshot of every data about jobs that wait to be started, are
* currently running or are done.
*
* It is possible to get only running, waiting or done jobs by giving
* "running", "waiting" or "done" as fist argument.
*
* It is also possible to get every job with a specified id by giving it as
* first argument. Also, using a RegExp instead of an id will return every
* jobs whose ids match the RegExp. And these two last use cases work as well
* by giving before "running", "waiting" or "done".
*
* @return {Array} The array of the matching jobs.
*
* Some call examples:
* *******************
* > conrad.getStats('running')
* > conrad.getStats('waiting')
* > conrad.getStats('done')
* > conrad.getStats('myJob')
* > conrad.getStats(/test/)
* > conrad.getStats('running', 'myRunningJob')
* > conrad.getStats('running', /test/)
*/
function _getStats(v1, v2) {
var a,
k,
i,
l,
stats,
pattern,
isPatternString;
if (!arguments.length) {
stats = [];
for (k in _jobs)
stats.push(_jobs[k]);
for (k in _waitingJobs)
stats.push(_waitingJobs[k]);
for (k in _runningJobs)
stats.push(_runningJobs[k]);
stats = stats.concat(_doneJobs);
}
if (typeof v1 === 'string')
switch (v1) {
case 'waiting':
stats = __objectValues(_waitingJobs);
break;
case 'running':
stats = __objectValues(_runningJobs);
break;
case 'done':
stats = _doneJobs;
break;
default:
pattern = v1;
}
if (v1 instanceof RegExp)
pattern = v1;
if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp))
pattern = v2;
// Filter jobs if a pattern is given:
if (pattern) {
isPatternString = typeof pattern === 'string';
if (stats instanceof Array) {
a = stats;
} else if (typeof stats === 'object') {
a = [];
for (k in stats)
a = a.concat(stats[k]);
} else {
a = [];
for (k in _jobs)
a.push(_jobs[k]);
for (k in _waitingJobs)
a.push(_waitingJobs[k]);
for (k in _runningJobs)
a.push(_runningJobs[k]);
a = a.concat(_doneJobs);
}
stats = [];
for (i = 0, l = a.length; i < l; i++)
if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern))
stats.push(a[i]);
}
return __clone(stats);
}
/**
* TOOLS FUNCTIONS:
* ****************
*/
/**
* This function takes any number of objects as arguments, copies from each
* of these objects each pair key/value into a new object, and finally
* returns this object.
*
* The arguments are parsed from the last one to the first one, such that
* when two objects have keys in common, the "earliest" object wins.
*
* Example:
* ********
* > var o1 = {
* > a: 1,
* > b: 2,
* > c: '3'
* > },
* > o2 = {
* > c: '4',
* > d: [ 5 ]
* > };
* > __extend(o1, o2);
* > // Returns: {
* > // a: 1,
* > // b: 2,
* > // c: '3',
* > // d: [ 5 ]
* > // };
*
* @param {Object+} Any number of objects.
* @return {Object} The merged object.
*/
function __extend() {
var i,
k,
res = {},
l = arguments.length;
for (i = l - 1; i >= 0; i--)
for (k in arguments[i])
res[k] = arguments[i][k];
return res;
}
/**
* This function simply clones an object. This object must contain only
* objects, arrays and immutable values. Since it is not public, it does not
* deal with cyclic references, DOM elements and instantiated objects - so
* use it carefully.
*
* @param {Object} The object to clone.
* @return {Object} The clone.
*/
function __clone(item) {
var result, i, k, l;
if (!item)
return item;
if (Array.isArray(item)) {
result = [];
for (i = 0, l = item.length; i < l; i++)
result.push(__clone(item[i]));
} else if (typeof item === 'object') {
result = {};
for (i in item)
result[i] = __clone(item[i]);
} else
result = item;
return result;
}
/**
* Returns an array containing the values of an object.
*
* @param {Object} The object.
* @return {Array} The array of values.
*/
function __objectValues(o) {
var k,
a = [];
for (k in o)
a.push(o[k]);
return a;
}
/**
* A short "Date.now()" polyfill.
*
* @return {Number} The current time (in ms).
*/
function __dateNow() {
return Date.now ? Date.now() : new Date().getTime();
}
/**
* Polyfill for the Array.isArray function:
*/
if (!Array.isArray)
Array.isArray = function(v) {
return Object.prototype.toString.call(v) === '[object Array]';
};
/**
* EXPORT PUBLIC API:
* ******************
*/
var conrad = {
hasJob: _hasJob,
addJob: _addJob,
killJob: _killJob,
killAll: _killAll,
settings: _settings,
getStats: _getStats,
isRunning: _getIsRunning,
clearHistory: _clearHistory,
// Events management:
bind: _bind,
unbind: _unbind,
// Version:
version: '0.1.0'
};
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports)
exports = module.exports = conrad;
exports.conrad = conrad;
}
global.conrad = conrad;
})(this);
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
var _root = this;
// Initialize packages:
sigma.utils = sigma.utils || {};
/**
* MISC UTILS:
*/
/**
* This function takes any number of objects as arguments, copies from each
* of these objects each pair key/value into a new object, and finally
* returns this object.
*
* The arguments are parsed from the last one to the first one, such that
* when several objects have keys in common, the "earliest" object wins.
*
* Example:
* ********
* > var o1 = {
* > a: 1,
* > b: 2,
* > c: '3'
* > },
* > o2 = {
* > c: '4',
* > d: [ 5 ]
* > };
* > sigma.utils.extend(o1, o2);
* > // Returns: {
* > // a: 1,
* > // b: 2,
* > // c: '3',
* > // d: [ 5 ]
* > // };
*
* @param {object+} Any number of objects.
* @return {object} The merged object.
*/
sigma.utils.extend = function() {
var i,
k,
res = {},
l = arguments.length;
for (i = l - 1; i >= 0; i--)
for (k in arguments[i])
res[k] = arguments[i][k];
return res;
};
/**
* A short "Date.now()" polyfill.
*
* @return {Number} The current time (in ms).
*/
sigma.utils.dateNow = function() {
return Date.now ? Date.now() : new Date().getTime();
};
/**
* Takes a package name as parameter and checks at each lebel if it exists,
* and if it does not, creates it.
*
* Example:
* ********
* > sigma.utils.pkg('a.b.c');
* > a.b.c;
* > // Object {};
* >
* > sigma.utils.pkg('a.b.d');
* > a.b;
* > // Object { c: {}, d: {} };
*
* @param {string} pkgName The name of the package to create/find.
* @return {object} The related package.
*/
sigma.utils.pkg = function(pkgName) {
return (pkgName || '').split('.').reduce(function(context, objName) {
return (objName in context) ?
context[objName] :
(context[objName] = {});
}, _root);
};
/**
* Returns a unique incremental number ID.
*
* Example:
* ********
* > sigma.utils.id();
* > // 1;
* >
* > sigma.utils.id();
* > // 2;
* >
* > sigma.utils.id();
* > // 3;
*
* @param {string} pkgName The name of the package to create/find.
* @return {object} The related package.
*/
sigma.utils.id = (function() {
var i = 0;
return function() {
return ++i;
};
})();
/**
* This function takes an hexa color (for instance "#ffcc00" or "#fc0") or a
* rgb / rgba color (like "rgb(255,255,12)" or "rgba(255,255,12,1)") and
* returns an integer equal to "r * 255 * 255 + g * 255 + b", to gain some
* memory in the data given to WebGL shaders.
*
* Note that the function actually caches its results for better performance.
*
* @param {string} val The hexa or rgba color.
* @return {number} The number value.
*/
var floatColorCache = {};
sigma.utils.floatColor = function(val) {
// Is the color already computed?
if (floatColorCache[val])
return floatColorCache[val];
var original = val,
r = 0,
g = 0,
b = 0;
if (val[0] === '#') {
val = val.slice(1);
if (val.length === 3) {
r = parseInt(val.charAt(0) + val.charAt(0), 16);
g = parseInt(val.charAt(1) + val.charAt(1), 16);
b = parseInt(val.charAt(2) + val.charAt(2), 16);
}
else {
r = parseInt(val.charAt(0) + val.charAt(1), 16);
g = parseInt(val.charAt(2) + val.charAt(3), 16);
b = parseInt(val.charAt(4) + val.charAt(5), 16);
}
} else if (val.match(/^ *rgba? *\(/)) {
val = val.match(
/^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.