UNPKG

ccnetviz

Version:

[![Build Status](https://travis-ci.org/HelikarLab/ccNetViz.svg?branch=master)](https://travis-ci.org/HelikarLab/ccNetViz) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![semantic-releas

1,880 lines (1,635 loc) 352 kB
;(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]*) *(,.