;(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.1'; /** * EXPORT: * ******* */ if (typeof this.sigma !== 'undefined') throw 'An object called sigma is already in the global scope.'; this.sigma = sigma; }).call(this);