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