;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw 'sigma is not declared'; if (typeof conrad === 'undefined') throw 'conrad is not declared'; // Initialize packages: sigma.utils.pkg('sigma.renderers'); /** * This function is the constructor of the svg sigma's renderer. * * @param {sigma.classes.graph} graph The graph to render. * @param {sigma.classes.camera} camera The camera. * @param {configurable} settings The sigma instance settings * function. * @param {object} object The options object. * @return {sigma.renderers.svg} The renderer instance. */ sigma.renderers.svg = function(graph, camera, settings, options) { if (typeof options !== 'object') throw 'sigma.renderers.svg: Wrong arguments.'; if (!(options.container instanceof HTMLElement)) throw 'Container not found.'; var i, l, a, fn, self = this; sigma.classes.dispatcher.extend(this); // Initialize main attributes: this.graph = graph; this.camera = camera; this.domElements = { graph: null, groups: {}, nodes: {}, edges: {}, labels: {}, hovers: {} }; this.measurementCanvas = null; this.options = options; this.container = this.options.container; this.settings = ( typeof options.settings === 'object' && options.settings ) ? settings.embedObjects(options.settings) : settings; // Is the renderer meant to be freestyle? this.settings('freeStyle', !!this.options.freeStyle); // SVG xmlns this.settings('xmlns', 'http://www.w3.org/2000/svg'); // Indexes: this.nodesOnScreen = []; this.edgesOnScreen = []; // Find the prefix: this.options.prefix = 'renderer' + sigma.utils.id() + ':'; // Initialize the DOM elements this.initDOM('svg'); // Initialize captors: this.captors = []; a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch]; for (i = 0, l = a.length; i < l; i++) { fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]]; this.captors.push( new fn( this.domElements.graph, this.camera, this.settings ) ); } // Bind resize: window.addEventListener('resize', function() { self.resize(); }); // Deal with sigma events: // TODO: keep an option to override the DOM events? sigma.misc.bindDOMEvents.call(this, this.domElements.graph); this.bindHovers(this.options.prefix); // Resize this.resize(false); }; /** * This method renders the graph on the svg scene. * * @param {?object} options Eventually an object of options. * @return {sigma.renderers.svg} Returns the instance itself. */ sigma.renderers.svg.prototype.render = function(options) { options = options || {}; var a, i, k, e, l, o, source, target, start, edges, renderers, subrenderers, index = {}, graph = this.graph, nodes = this.graph.nodes, prefix = this.options.prefix || '', drawEdges = this.settings(options, 'drawEdges'), drawNodes = this.settings(options, 'drawNodes'), drawLabels = this.settings(options, 'drawLabels'), embedSettings = this.settings.embedObjects(options, { prefix: this.options.prefix, forceLabels: this.options.forceLabels }); // Check the 'hideEdgesOnMove' setting: if (this.settings(options, 'hideEdgesOnMove')) if (this.camera.isAnimated || this.camera.isMoving) drawEdges = false; // Apply the camera's view: this.camera.applyView( undefined, this.options.prefix, { width: this.width, height: this.height } ); // Hiding everything // TODO: find a more sensible way to perform this operation this.hideDOMElements(this.domElements.nodes); this.hideDOMElements(this.domElements.edges); this.hideDOMElements(this.domElements.labels); // Find which nodes are on screen this.edgesOnScreen = []; this.nodesOnScreen = this.camera.quadtree.area( this.camera.getRectangle(this.width, this.height) ); // Node index for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) index[a[i].id] = a[i]; // Find which edges are on screen for (a = graph.edges(), i = 0, l = a.length; i < l; i++) { o = a[i]; if ( (index[o.source] || index[o.target]) && (!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden) ) this.edgesOnScreen.push(o); } // Display nodes //--------------- renderers = sigma.svg.nodes; subrenderers = sigma.svg.labels; //-- First we create the nodes which are not already created if (drawNodes) for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) { if (!a[i].hidden && !this.domElements.nodes[a[i].id]) { // Node e = (renderers[a[i].type] || renderers.def).create( a[i], embedSettings ); this.domElements.nodes[a[i].id] = e; this.domElements.groups.nodes.appendChild(e); // Label e = (subrenderers[a[i].type] || subrenderers.def).create( a[i], embedSettings ); this.domElements.labels[a[i].id] = e; this.domElements.groups.labels.appendChild(e); } } //-- Second we update the nodes if (drawNodes) for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) { if (a[i].hidden) continue; // Node (renderers[a[i].type] || renderers.def).update( a[i], this.domElements.nodes[a[i].id], embedSettings ); // Label (subrenderers[a[i].type] || subrenderers.def).update( a[i], this.domElements.labels[a[i].id], embedSettings ); } // Display edges //--------------- renderers = sigma.svg.edges; //-- First we create the edges which are not already created if (drawEdges) for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) { if (!this.domElements.edges[a[i].id]) { source = nodes(a[i].source); target = nodes(a[i].target); e = (renderers[a[i].type] || renderers.def).create( a[i], source, target, embedSettings ); this.domElements.edges[a[i].id] = e; this.domElements.groups.edges.appendChild(e); } } //-- Second we update the edges if (drawEdges) for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) { source = nodes(a[i].source); target = nodes(a[i].target); (renderers[a[i].type] || renderers.def).update( a[i], this.domElements.edges[a[i].id], source, target, embedSettings ); } this.dispatchEvent('render'); return this; }; /** * This method creates a DOM element of the specified type, switches its * position to "absolute", references it to the domElements attribute, and * finally appends it to the container. * * @param {string} tag The label tag. * @param {string} id The id of the element (to store it in "domElements"). */ sigma.renderers.svg.prototype.initDOM = function(tag) { var dom = document.createElementNS(this.settings('xmlns'), tag), c = this.settings('classPrefix'), g, l, i; dom.style.position = 'absolute'; dom.setAttribute('class', c + '-svg'); // Setting SVG namespace dom.setAttribute('xmlns', this.settings('xmlns')); dom.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); dom.setAttribute('version', '1.1'); // Creating the measurement canvas var canvas = document.createElement('canvas'); canvas.setAttribute('class', c + '-measurement-canvas'); // Appending elements this.domElements.graph = this.container.appendChild(dom); // Creating groups var groups = ['edges', 'nodes', 'labels', 'hovers']; for (i = 0, l = groups.length; i < l; i++) { g = document.createElementNS(this.settings('xmlns'), 'g'); g.setAttributeNS(null, 'id', c + '-group-' + groups[i]); g.setAttributeNS(null, 'class', c + '-group'); this.domElements.groups[groups[i]] = this.domElements.graph.appendChild(g); } // Appending measurement canvas this.container.appendChild(canvas); this.measurementCanvas = canvas.getContext('2d'); }; /** * This method hides a batch of SVG DOM elements. * * @param {array} elements An array of elements to hide. * @param {object} renderer The renderer to use. * @return {sigma.renderers.svg} Returns the instance itself. */ sigma.renderers.svg.prototype.hideDOMElements = function(elements) { var o, i; for (i in elements) { o = elements[i]; sigma.svg.utils.hide(o); } return this; }; /** * This method binds the hover events to the renderer. * * @param {string} prefix The renderer prefix. */ // TODO: add option about whether to display hovers or not sigma.renderers.svg.prototype.bindHovers = function(prefix) { var renderers = sigma.svg.hovers, self = this, hoveredNode; function overNode(e) { var node = e.data.node, embedSettings = self.settings.embedObjects({ prefix: prefix }); if (!embedSettings('enableHovering')) return; var hover = (renderers[node.type] || renderers.def).create( node, self.domElements.nodes[node.id], self.measurementCanvas, embedSettings ); self.domElements.hovers[node.id] = hover; // Inserting the hover in the dom self.domElements.groups.hovers.appendChild(hover); hoveredNode = node; } function outNode(e) { var node = e.data.node, embedSettings = self.settings.embedObjects({ prefix: prefix }); if (!embedSettings('enableHovering')) return; // Deleting element self.domElements.groups.hovers.removeChild( self.domElements.hovers[node.id] ); hoveredNode = null; delete self.domElements.hovers[node.id]; // Reinstate self.domElements.groups.nodes.appendChild( self.domElements.nodes[node.id] ); } // OPTIMIZE: perform a real update rather than a deletion function update() { if (!hoveredNode) return; var embedSettings = self.settings.embedObjects({ prefix: prefix }); // Deleting element before update self.domElements.groups.hovers.removeChild( self.domElements.hovers[hoveredNode.id] ); delete self.domElements.hovers[hoveredNode.id]; var hover = (renderers[hoveredNode.type] || renderers.def).create( hoveredNode, self.domElements.nodes[hoveredNode.id], self.measurementCanvas, embedSettings ); self.domElements.hovers[hoveredNode.id] = hover; // Inserting the hover in the dom self.domElements.groups.hovers.appendChild(hover); } // Binding events this.bind('overNode', overNode); this.bind('outNode', outNode); // Update on render this.bind('render', update); }; /** * This method resizes each DOM elements in the container and stores the new * dimensions. Then, it renders the graph. * * @param {?number} width The new width of the container. * @param {?number} height The new height of the container. * @return {sigma.renderers.svg} Returns the instance itself. */ sigma.renderers.svg.prototype.resize = function(w, h) { var oldWidth = this.width, oldHeight = this.height, pixelRatio = 1; if (w !== undefined && h !== undefined) { this.width = w; this.height = h; } else { this.width = this.container.offsetWidth; this.height = this.container.offsetHeight; w = this.width; h = this.height; } if (oldWidth !== this.width || oldHeight !== this.height) { this.domElements.graph.style.width = w + 'px'; this.domElements.graph.style.height = h + 'px'; if (this.domElements.graph.tagName.toLowerCase() === 'svg') { this.domElements.graph.setAttribute('width', (w * pixelRatio)); this.domElements.graph.setAttribute('height', (h * pixelRatio)); } } return this; }; /** * The labels, nodes and edges renderers are stored in the three following * objects. When an element is drawn, its type will be checked and if a * renderer with the same name exists, it will be used. If not found, the * default renderer will be used instead. * * They are stored in different files, in the "./svg" folder. */ sigma.utils.pkg('sigma.svg.nodes'); sigma.utils.pkg('sigma.svg.edges'); sigma.utils.pkg('sigma.svg.labels'); }).call(this);