Personal blog written from scratch using Node.js, Bootstrap, and MySQL. https://jrtechs.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

739 lines
20 KiB

  1. ;(function(undefined) {
  2. 'use strict';
  3. var __instances = {};
  4. /**
  5. * This is the sigma instances constructor. One instance of sigma represent
  6. * one graph. It is possible to represent this grapĥ with several renderers
  7. * at the same time. By default, the default renderer (WebGL + Canvas
  8. * polyfill) will be used as the only renderer, with the container specified
  9. * in the configuration.
  10. *
  11. * @param {?*} conf The configuration of the instance. There are a lot of
  12. * different recognized forms to instantiate sigma, check
  13. * example files, documentation in this file and unit
  14. * tests to know more.
  15. * @return {sigma} The fresh new sigma instance.
  16. *
  17. * Instanciating sigma:
  18. * ********************
  19. * If no parameter is given to the constructor, the instance will be created
  20. * without any renderer or camera. It will just instantiate the graph, and
  21. * other modules will have to be instantiated through the public methods,
  22. * like "addRenderer" etc:
  23. *
  24. * > s0 = new sigma();
  25. * > s0.addRenderer({
  26. * > type: 'canvas',
  27. * > container: 'my-container-id'
  28. * > });
  29. *
  30. * In most of the cases, sigma will simply be used with the default renderer.
  31. * Then, since the only required parameter is the DOM container, there are
  32. * some simpler way to call the constructor. The four following calls do the
  33. * exact same things:
  34. *
  35. * > s1 = new sigma('my-container-id');
  36. * > s2 = new sigma(document.getElementById('my-container-id'));
  37. * > s3 = new sigma({
  38. * > container: document.getElementById('my-container-id')
  39. * > });
  40. * > s4 = new sigma({
  41. * > renderers: [{
  42. * > container: document.getElementById('my-container-id')
  43. * > }]
  44. * > });
  45. *
  46. * Recognized parameters:
  47. * **********************
  48. * Here is the exhaustive list of every accepted parameters, when calling the
  49. * constructor with to top level configuration object (fourth case in the
  50. * previous examples):
  51. *
  52. * {?string} id The id of the instance. It will be generated
  53. * automatically if not specified.
  54. * {?array} renderers An array containing objects describing renderers.
  55. * {?object} graph An object containing an array of nodes and an array
  56. * of edges, to avoid having to add them by hand later.
  57. * {?object} settings An object containing instance specific settings that
  58. * will override the default ones defined in the object
  59. * sigma.settings.
  60. */
  61. var sigma = function(conf) {
  62. // Local variables:
  63. // ****************
  64. var i,
  65. l,
  66. a,
  67. c,
  68. o,
  69. id;
  70. sigma.classes.dispatcher.extend(this);
  71. // Private attributes:
  72. // *******************
  73. var _self = this,
  74. _conf = conf || {};
  75. // Little shortcut:
  76. // ****************
  77. // The configuration is supposed to have a list of the configuration
  78. // objects for each renderer.
  79. // - If there are no configuration at all, then nothing is done.
  80. // - If there are no renderer list, the given configuration object will be
  81. // considered as describing the first and only renderer.
  82. // - If there are no renderer list nor "container" object, it will be
  83. // considered as the container itself (a DOM element).
  84. // - If the argument passed to sigma() is a string, it will be considered
  85. // as the ID of the DOM container.
  86. if (
  87. typeof _conf === 'string' ||
  88. _conf instanceof HTMLElement
  89. )
  90. _conf = {
  91. renderers: [_conf]
  92. };
  93. else if (Object.prototype.toString.call(_conf) === '[object Array]')
  94. _conf = {
  95. renderers: _conf
  96. };
  97. // Also check "renderer" and "container" keys:
  98. o = _conf.renderers || _conf.renderer || _conf.container;
  99. if (!_conf.renderers || _conf.renderers.length === 0)
  100. if (
  101. typeof o === 'string' ||
  102. o instanceof HTMLElement ||
  103. (typeof o === 'object' && 'container' in o)
  104. )
  105. _conf.renderers = [o];
  106. // Recense the instance:
  107. if (_conf.id) {
  108. if (__instances[_conf.id])
  109. throw 'sigma: Instance "' + _conf.id + '" already exists.';
  110. Object.defineProperty(this, 'id', {
  111. value: _conf.id
  112. });
  113. } else {
  114. id = 0;
  115. while (__instances[id])
  116. id++;
  117. Object.defineProperty(this, 'id', {
  118. value: '' + id
  119. });
  120. }
  121. __instances[this.id] = this;
  122. // Initialize settings function:
  123. this.settings = new sigma.classes.configurable(
  124. sigma.settings,
  125. _conf.settings || {}
  126. );
  127. // Initialize locked attributes:
  128. Object.defineProperty(this, 'graph', {
  129. value: new sigma.classes.graph(this.settings),
  130. configurable: true
  131. });
  132. Object.defineProperty(this, 'middlewares', {
  133. value: [],
  134. configurable: true
  135. });
  136. Object.defineProperty(this, 'cameras', {
  137. value: {},
  138. configurable: true
  139. });
  140. Object.defineProperty(this, 'renderers', {
  141. value: {},
  142. configurable: true
  143. });
  144. Object.defineProperty(this, 'renderersPerCamera', {
  145. value: {},
  146. configurable: true
  147. });
  148. Object.defineProperty(this, 'cameraFrames', {
  149. value: {},
  150. configurable: true
  151. });
  152. Object.defineProperty(this, 'camera', {
  153. get: function() {
  154. return this.cameras[0];
  155. }
  156. });
  157. Object.defineProperty(this, 'events', {
  158. value: [
  159. 'click',
  160. 'rightClick',
  161. 'clickStage',
  162. 'doubleClickStage',
  163. 'rightClickStage',
  164. 'clickNode',
  165. 'clickNodes',
  166. 'doubleClickNode',
  167. 'doubleClickNodes',
  168. 'rightClickNode',
  169. 'rightClickNodes',
  170. 'overNode',
  171. 'overNodes',
  172. 'outNode',
  173. 'outNodes',
  174. 'downNode',
  175. 'downNodes',
  176. 'upNode',
  177. 'upNodes'
  178. ],
  179. configurable: true
  180. });
  181. // Add a custom handler, to redispatch events from renderers:
  182. this._handler = (function(e) {
  183. var k,
  184. data = {};
  185. for (k in e.data)
  186. data[k] = e.data[k];
  187. data.renderer = e.target;
  188. this.dispatchEvent(e.type, data);
  189. }).bind(this);
  190. // Initialize renderers:
  191. a = _conf.renderers || [];
  192. for (i = 0, l = a.length; i < l; i++)
  193. this.addRenderer(a[i]);
  194. // Initialize middlewares:
  195. a = _conf.middlewares || [];
  196. for (i = 0, l = a.length; i < l; i++)
  197. this.middlewares.push(
  198. typeof a[i] === 'string' ?
  199. sigma.middlewares[a[i]] :
  200. a[i]
  201. );
  202. // Check if there is already a graph to fill in:
  203. if (typeof _conf.graph === 'object' && _conf.graph) {
  204. this.graph.read(_conf.graph);
  205. // If a graph is given to the to the instance, the "refresh" method is
  206. // directly called:
  207. this.refresh();
  208. }
  209. // Deal with resize:
  210. window.addEventListener('resize', function() {
  211. if (_self.settings)
  212. _self.refresh();
  213. });
  214. };
  215. /**
  216. * This methods will instantiate and reference a new camera. If no id is
  217. * specified, then an automatic id will be generated.
  218. *
  219. * @param {?string} id Eventually the camera id.
  220. * @return {sigma.classes.camera} The fresh new camera instance.
  221. */
  222. sigma.prototype.addCamera = function(id) {
  223. var self = this,
  224. camera;
  225. if (!arguments.length) {
  226. id = 0;
  227. while (this.cameras['' + id])
  228. id++;
  229. id = '' + id;
  230. }
  231. if (this.cameras[id])
  232. throw 'sigma.addCamera: The camera "' + id + '" already exists.';
  233. camera = new sigma.classes.camera(id, this.graph, this.settings);
  234. this.cameras[id] = camera;
  235. // Add a quadtree to the camera:
  236. camera.quadtree = new sigma.classes.quad();
  237. // Add an edgequadtree to the camera:
  238. if (sigma.classes.edgequad !== undefined) {
  239. camera.edgequadtree = new sigma.classes.edgequad();
  240. }
  241. camera.bind('coordinatesUpdated', function(e) {
  242. self.renderCamera(camera, camera.isAnimated);
  243. });
  244. this.renderersPerCamera[id] = [];
  245. return camera;
  246. };
  247. /**
  248. * This method kills a camera, and every renderer attached to it.
  249. *
  250. * @param {string|camera} v The camera to kill or its ID.
  251. * @return {sigma} Returns the instance.
  252. */
  253. sigma.prototype.killCamera = function(v) {
  254. v = typeof v === 'string' ? this.cameras[v] : v;
  255. if (!v)
  256. throw 'sigma.killCamera: The camera is undefined.';
  257. var i,
  258. l,
  259. a = this.renderersPerCamera[v.id];
  260. for (l = a.length, i = l - 1; i >= 0; i--)
  261. this.killRenderer(a[i]);
  262. delete this.renderersPerCamera[v.id];
  263. delete this.cameraFrames[v.id];
  264. delete this.cameras[v.id];
  265. if (v.kill)
  266. v.kill();
  267. return this;
  268. };
  269. /**
  270. * This methods will instantiate and reference a new renderer. The "type"
  271. * argument can be the constructor or its name in the "sigma.renderers"
  272. * package. If no type is specified, then "sigma.renderers.def" will be used.
  273. * If no id is specified, then an automatic id will be generated.
  274. *
  275. * @param {?object} options Eventually some options to give to the renderer
  276. * constructor.
  277. * @return {renderer} The fresh new renderer instance.
  278. *
  279. * Recognized parameters:
  280. * **********************
  281. * Here is the exhaustive list of every accepted parameters in the "options"
  282. * object:
  283. *
  284. * {?string} id Eventually the renderer id.
  285. * {?(function|string)} type Eventually the renderer constructor or its
  286. * name in the "sigma.renderers" package.
  287. * {?(camera|string)} camera Eventually the renderer camera or its
  288. * id.
  289. */
  290. sigma.prototype.addRenderer = function(options) {
  291. var id,
  292. fn,
  293. camera,
  294. renderer,
  295. o = options || {};
  296. // Polymorphism:
  297. if (typeof o === 'string')
  298. o = {
  299. container: document.getElementById(o)
  300. };
  301. else if (o instanceof HTMLElement)
  302. o = {
  303. container: o
  304. };
  305. // If the container still is a string, we get it by id
  306. if (typeof o.container === 'string')
  307. o.container = document.getElementById(o.container);
  308. // Reference the new renderer:
  309. if (!('id' in o)) {
  310. id = 0;
  311. while (this.renderers['' + id])
  312. id++;
  313. id = '' + id;
  314. } else
  315. id = o.id;
  316. if (this.renderers[id])
  317. throw 'sigma.addRenderer: The renderer "' + id + '" already exists.';
  318. // Find the good constructor:
  319. fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type];
  320. fn = fn || sigma.renderers.def;
  321. // Find the good camera:
  322. camera = 'camera' in o ?
  323. (
  324. o.camera instanceof sigma.classes.camera ?
  325. o.camera :
  326. this.cameras[o.camera] || this.addCamera(o.camera)
  327. ) :
  328. this.addCamera();
  329. if (this.cameras[camera.id] !== camera)
  330. throw 'sigma.addRenderer: The camera is not properly referenced.';
  331. // Instantiate:
  332. renderer = new fn(this.graph, camera, this.settings, o);
  333. this.renderers[id] = renderer;
  334. Object.defineProperty(renderer, 'id', {
  335. value: id
  336. });
  337. // Bind events:
  338. if (renderer.bind)
  339. renderer.bind(
  340. [
  341. 'click',
  342. 'rightClick',
  343. 'clickStage',
  344. 'doubleClickStage',
  345. 'rightClickStage',
  346. 'clickNode',
  347. 'clickNodes',
  348. 'clickEdge',
  349. 'clickEdges',
  350. 'doubleClickNode',
  351. 'doubleClickNodes',
  352. 'doubleClickEdge',
  353. 'doubleClickEdges',
  354. 'rightClickNode',
  355. 'rightClickNodes',
  356. 'rightClickEdge',
  357. 'rightClickEdges',
  358. 'overNode',
  359. 'overNodes',
  360. 'overEdge',
  361. 'overEdges',
  362. 'outNode',
  363. 'outNodes',
  364. 'outEdge',
  365. 'outEdges',
  366. 'downNode',
  367. 'downNodes',
  368. 'downEdge',
  369. 'downEdges',
  370. 'upNode',
  371. 'upNodes',
  372. 'upEdge',
  373. 'upEdges'
  374. ],
  375. this._handler
  376. );
  377. // Reference the renderer by its camera:
  378. this.renderersPerCamera[camera.id].push(renderer);
  379. return renderer;
  380. };
  381. /**
  382. * This method kills a renderer.
  383. *
  384. * @param {string|renderer} v The renderer to kill or its ID.
  385. * @return {sigma} Returns the instance.
  386. */
  387. sigma.prototype.killRenderer = function(v) {
  388. v = typeof v === 'string' ? this.renderers[v] : v;
  389. if (!v)
  390. throw 'sigma.killRenderer: The renderer is undefined.';
  391. var a = this.renderersPerCamera[v.camera.id],
  392. i = a.indexOf(v);
  393. if (i >= 0)
  394. a.splice(i, 1);
  395. if (v.kill)
  396. v.kill();
  397. delete this.renderers[v.id];
  398. return this;
  399. };
  400. /**
  401. * This method calls the "render" method of each renderer, with the same
  402. * arguments than the "render" method, but will also check if the renderer
  403. * has a "process" method, and call it if it exists.
  404. *
  405. * It is useful for quadtrees or WebGL processing, for instance.
  406. *
  407. * @param {?object} options Eventually some options to give to the refresh
  408. * method.
  409. * @return {sigma} Returns the instance itself.
  410. *
  411. * Recognized parameters:
  412. * **********************
  413. * Here is the exhaustive list of every accepted parameters in the "options"
  414. * object:
  415. *
  416. * {?boolean} skipIndexation A flag specifying wether or not the refresh
  417. * function should reindex the graph in the
  418. * quadtrees or not (default: false).
  419. */
  420. sigma.prototype.refresh = function(options) {
  421. var i,
  422. l,
  423. k,
  424. a,
  425. c,
  426. bounds,
  427. prefix = 0;
  428. options = options || {};
  429. // Call each middleware:
  430. a = this.middlewares || [];
  431. for (i = 0, l = a.length; i < l; i++)
  432. a[i].call(
  433. this,
  434. (i === 0) ? '' : 'tmp' + prefix + ':',
  435. (i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':')
  436. );
  437. // Then, for each camera, call the "rescale" middleware, unless the
  438. // settings specify not to:
  439. for (k in this.cameras) {
  440. c = this.cameras[k];
  441. if (
  442. c.settings('autoRescale') &&
  443. this.renderersPerCamera[c.id] &&
  444. this.renderersPerCamera[c.id].length
  445. )
  446. sigma.middlewares.rescale.call(
  447. this,
  448. a.length ? 'ready:' : '',
  449. c.readPrefix,
  450. {
  451. width: this.renderersPerCamera[c.id][0].width,
  452. height: this.renderersPerCamera[c.id][0].height
  453. }
  454. );
  455. else
  456. sigma.middlewares.copy.call(
  457. this,
  458. a.length ? 'ready:' : '',
  459. c.readPrefix
  460. );
  461. if (!options.skipIndexation) {
  462. // Find graph boundaries:
  463. bounds = sigma.utils.getBoundaries(
  464. this.graph,
  465. c.readPrefix
  466. );
  467. // Refresh quadtree:
  468. c.quadtree.index(this.graph.nodes(), {
  469. prefix: c.readPrefix,
  470. bounds: {
  471. x: bounds.minX,
  472. y: bounds.minY,
  473. width: bounds.maxX - bounds.minX,
  474. height: bounds.maxY - bounds.minY
  475. }
  476. });
  477. // Refresh edgequadtree:
  478. if (
  479. c.edgequadtree !== undefined &&
  480. c.settings('drawEdges') &&
  481. c.settings('enableEdgeHovering')
  482. ) {
  483. c.edgequadtree.index(this.graph, {
  484. prefix: c.readPrefix,
  485. bounds: {
  486. x: bounds.minX,
  487. y: bounds.minY,
  488. width: bounds.maxX - bounds.minX,
  489. height: bounds.maxY - bounds.minY
  490. }
  491. });
  492. }
  493. }
  494. }
  495. // Call each renderer:
  496. a = Object.keys(this.renderers);
  497. for (i = 0, l = a.length; i < l; i++)
  498. if (this.renderers[a[i]].process) {
  499. if (this.settings('skipErrors'))
  500. try {
  501. this.renderers[a[i]].process();
  502. } catch (e) {
  503. console.log(
  504. 'Warning: The renderer "' + a[i] + '" crashed on ".process()"'
  505. );
  506. }
  507. else
  508. this.renderers[a[i]].process();
  509. }
  510. this.render();
  511. return this;
  512. };
  513. /**
  514. * This method calls the "render" method of each renderer.
  515. *
  516. * @return {sigma} Returns the instance itself.
  517. */
  518. sigma.prototype.render = function() {
  519. var i,
  520. l,
  521. a,
  522. prefix = 0;
  523. // Call each renderer:
  524. a = Object.keys(this.renderers);
  525. for (i = 0, l = a.length; i < l; i++)
  526. if (this.settings('skipErrors'))
  527. try {
  528. this.renderers[a[i]].render();
  529. } catch (e) {
  530. if (this.settings('verbose'))
  531. console.log(
  532. 'Warning: The renderer "' + a[i] + '" crashed on ".render()"'
  533. );
  534. }
  535. else
  536. this.renderers[a[i]].render();
  537. return this;
  538. };
  539. /**
  540. * This method calls the "render" method of each renderer that is bound to
  541. * the specified camera. To improve the performances, if this method is
  542. * called too often, the number of effective renderings is limitated to one
  543. * per frame, unless you are using the "force" flag.
  544. *
  545. * @param {sigma.classes.camera} camera The camera to render.
  546. * @param {?boolean} force If true, will render the camera
  547. * directly.
  548. * @return {sigma} Returns the instance itself.
  549. */
  550. sigma.prototype.renderCamera = function(camera, force) {
  551. var i,
  552. l,
  553. a,
  554. self = this;
  555. if (force) {
  556. a = this.renderersPerCamera[camera.id];
  557. for (i = 0, l = a.length; i < l; i++)
  558. if (this.settings('skipErrors'))
  559. try {
  560. a[i].render();
  561. } catch (e) {
  562. if (this.settings('verbose'))
  563. console.log(
  564. 'Warning: The renderer "' + a[i].id + '" crashed on ".render()"'
  565. );
  566. }
  567. else
  568. a[i].render();
  569. } else {
  570. if (!this.cameraFrames[camera.id]) {
  571. a = this.renderersPerCamera[camera.id];
  572. for (i = 0, l = a.length; i < l; i++)
  573. if (this.settings('skipErrors'))
  574. try {
  575. a[i].render();
  576. } catch (e) {
  577. if (this.settings('verbose'))
  578. console.log(
  579. 'Warning: The renderer "' +
  580. a[i].id +
  581. '" crashed on ".render()"'
  582. );
  583. }
  584. else
  585. a[i].render();
  586. this.cameraFrames[camera.id] = requestAnimationFrame(function() {
  587. delete self.cameraFrames[camera.id];
  588. });
  589. }
  590. }
  591. return this;
  592. };
  593. /**
  594. * This method calls the "kill" method of each module and destroys any
  595. * reference from the instance.
  596. */
  597. sigma.prototype.kill = function() {
  598. var k;
  599. // Dispatching event
  600. this.dispatchEvent('kill');
  601. // Kill graph:
  602. this.graph.kill();
  603. // Kill middlewares:
  604. delete this.middlewares;
  605. // Kill each renderer:
  606. for (k in this.renderers)
  607. this.killRenderer(this.renderers[k]);
  608. // Kill each camera:
  609. for (k in this.cameras)
  610. this.killCamera(this.cameras[k]);
  611. delete this.renderers;
  612. delete this.cameras;
  613. // Kill everything else:
  614. for (k in this)
  615. if (this.hasOwnProperty(k))
  616. delete this[k];
  617. delete __instances[this.id];
  618. };
  619. /**
  620. * Returns a clone of the instances object or a specific running instance.
  621. *
  622. * @param {?string} id Eventually an instance ID.
  623. * @return {object} The related instance or a clone of the instances
  624. * object.
  625. */
  626. sigma.instances = function(id) {
  627. return arguments.length ?
  628. __instances[id] :
  629. sigma.utils.extend({}, __instances);
  630. };
  631. /**
  632. * The current version of sigma:
  633. */
  634. sigma.version = '1.2.1';
  635. /**
  636. * EXPORT:
  637. * *******
  638. */
  639. if (typeof this.sigma !== 'undefined')
  640. throw 'An object called sigma is already in the global scope.';
  641. this.sigma = sigma;
  642. }).call(this);