NodesHandler.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. let util = require("../../util");
  2. let DataSet = require('../../DataSet');
  3. let DataView = require('../../DataView');
  4. var Node = require("./components/Node").default;
  5. /**
  6. * Handler for Nodes
  7. */
  8. class NodesHandler {
  9. /**
  10. * @param {Object} body
  11. * @param {Images} images
  12. * @param {Array.<Group>} groups
  13. * @param {LayoutEngine} layoutEngine
  14. */
  15. constructor(body, images, groups, layoutEngine) {
  16. this.body = body;
  17. this.images = images;
  18. this.groups = groups;
  19. this.layoutEngine = layoutEngine;
  20. // create the node API in the body container
  21. this.body.functions.createNode = this.create.bind(this);
  22. this.nodesListeners = {
  23. add: (event, params) => { this.add(params.items); },
  24. update: (event, params) => { this.update(params.items, params.data, params.oldData); },
  25. remove: (event, params) => { this.remove(params.items); }
  26. };
  27. this.defaultOptions = {
  28. borderWidth: 1,
  29. borderWidthSelected: 2,
  30. brokenImage: undefined,
  31. color: {
  32. border: '#2B7CE9',
  33. background: '#97C2FC',
  34. highlight: {
  35. border: '#2B7CE9',
  36. background: '#D2E5FF'
  37. },
  38. hover: {
  39. border: '#2B7CE9',
  40. background: '#D2E5FF'
  41. }
  42. },
  43. fixed: {
  44. x: false,
  45. y: false
  46. },
  47. font: {
  48. color: '#343434',
  49. size: 14, // px
  50. face: 'arial',
  51. background: 'none',
  52. strokeWidth: 0, // px
  53. strokeColor: '#ffffff',
  54. align: 'center',
  55. vadjust: 0,
  56. multi: false,
  57. bold: {
  58. mod: 'bold'
  59. },
  60. boldital: {
  61. mod: 'bold italic'
  62. },
  63. ital: {
  64. mod: 'italic'
  65. },
  66. mono: {
  67. mod: '',
  68. size: 15, // px
  69. face: 'monospace',
  70. vadjust: 2
  71. }
  72. },
  73. group: undefined,
  74. hidden: false,
  75. icon: {
  76. face: 'FontAwesome', //'FontAwesome',
  77. code: undefined, //'\uf007',
  78. size: 50, //50,
  79. color: '#2B7CE9' //'#aa00ff'
  80. },
  81. image: undefined, // --> URL
  82. label: undefined,
  83. labelHighlightBold: true,
  84. level: undefined,
  85. margin: {
  86. top: 5,
  87. right: 5,
  88. bottom: 5,
  89. left: 5
  90. },
  91. mass: 1,
  92. physics: true,
  93. scaling: {
  94. min: 10,
  95. max: 30,
  96. label: {
  97. enabled: false,
  98. min: 14,
  99. max: 30,
  100. maxVisible: 30,
  101. drawThreshold: 5
  102. },
  103. customScalingFunction: function (min, max, total, value) {
  104. if (max === min) {
  105. return 0.5;
  106. }
  107. else {
  108. let scale = 1 / (max - min);
  109. return Math.max(0, (value - min) * scale);
  110. }
  111. }
  112. },
  113. shadow: {
  114. enabled: false,
  115. color: 'rgba(0,0,0,0.5)',
  116. size: 10,
  117. x: 5,
  118. y: 5
  119. },
  120. shape: 'ellipse',
  121. shapeProperties: {
  122. borderDashes: false, // only for borders
  123. borderRadius: 6, // only for box shape
  124. interpolation: true, // only for image and circularImage shapes
  125. useImageSize: false, // only for image and circularImage shapes
  126. useBorderWithImage: false // only for image shape
  127. },
  128. size: 25,
  129. title: undefined,
  130. value: undefined,
  131. x: undefined,
  132. y: undefined
  133. };
  134. // Protect from idiocy
  135. if (this.defaultOptions.mass <= 0) {
  136. throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative';
  137. }
  138. this.options = util.bridgeObject(this.defaultOptions);
  139. this.bindEventListeners();
  140. }
  141. /**
  142. * Binds event listeners
  143. */
  144. bindEventListeners() {
  145. // refresh the nodes. Used when reverting from hierarchical layout
  146. this.body.emitter.on('refreshNodes', this.refresh.bind(this));
  147. this.body.emitter.on('refresh', this.refresh.bind(this));
  148. this.body.emitter.on('destroy', () => {
  149. util.forEach(this.nodesListeners, (callback, event) => {
  150. if (this.body.data.nodes)
  151. this.body.data.nodes.off(event, callback);
  152. });
  153. delete this.body.functions.createNode;
  154. delete this.nodesListeners.add;
  155. delete this.nodesListeners.update;
  156. delete this.nodesListeners.remove;
  157. delete this.nodesListeners;
  158. });
  159. }
  160. /**
  161. *
  162. * @param {Object} options
  163. */
  164. setOptions(options) {
  165. if (options !== undefined) {
  166. Node.parseOptions(this.options, options);
  167. // update the shape in all nodes
  168. if (options.shape !== undefined) {
  169. for (let nodeId in this.body.nodes) {
  170. if (this.body.nodes.hasOwnProperty(nodeId)) {
  171. this.body.nodes[nodeId].updateShape();
  172. }
  173. }
  174. }
  175. // update the font in all nodes
  176. if (options.font !== undefined) {
  177. for (let nodeId in this.body.nodes) {
  178. if (this.body.nodes.hasOwnProperty(nodeId)) {
  179. this.body.nodes[nodeId].updateLabelModule();
  180. this.body.nodes[nodeId].needsRefresh();
  181. }
  182. }
  183. }
  184. // update the shape size in all nodes
  185. if (options.size !== undefined) {
  186. for (let nodeId in this.body.nodes) {
  187. if (this.body.nodes.hasOwnProperty(nodeId)) {
  188. this.body.nodes[nodeId].needsRefresh();
  189. }
  190. }
  191. }
  192. // update the state of the variables if needed
  193. if (options.hidden !== undefined || options.physics !== undefined) {
  194. this.body.emitter.emit('_dataChanged');
  195. }
  196. }
  197. }
  198. /**
  199. * Set a data set with nodes for the network
  200. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  201. * @param {boolean} [doNotEmit=false]
  202. * @private
  203. */
  204. setData(nodes, doNotEmit = false) {
  205. let oldNodesData = this.body.data.nodes;
  206. if (nodes instanceof DataSet || nodes instanceof DataView) {
  207. this.body.data.nodes = nodes;
  208. }
  209. else if (Array.isArray(nodes)) {
  210. this.body.data.nodes = new DataSet();
  211. this.body.data.nodes.add(nodes);
  212. }
  213. else if (!nodes) {
  214. this.body.data.nodes = new DataSet();
  215. }
  216. else {
  217. throw new TypeError('Array or DataSet expected');
  218. }
  219. if (oldNodesData) {
  220. // unsubscribe from old dataset
  221. util.forEach(this.nodesListeners, function (callback, event) {
  222. oldNodesData.off(event, callback);
  223. });
  224. }
  225. // remove drawn nodes
  226. this.body.nodes = {};
  227. if (this.body.data.nodes) {
  228. // subscribe to new dataset
  229. let me = this;
  230. util.forEach(this.nodesListeners, function (callback, event) {
  231. me.body.data.nodes.on(event, callback);
  232. });
  233. // draw all new nodes
  234. let ids = this.body.data.nodes.getIds();
  235. this.add(ids, true);
  236. }
  237. if (doNotEmit === false) {
  238. this.body.emitter.emit("_dataChanged");
  239. }
  240. }
  241. /**
  242. * Add nodes
  243. * @param {number[] | string[]} ids
  244. * @param {boolean} [doNotEmit=false]
  245. * @private
  246. */
  247. add(ids, doNotEmit = false) {
  248. let id;
  249. let newNodes = [];
  250. for (let i = 0; i < ids.length; i++) {
  251. id = ids[i];
  252. let properties = this.body.data.nodes.get(id);
  253. let node = this.create(properties);
  254. newNodes.push(node);
  255. this.body.nodes[id] = node; // note: this may replace an existing node
  256. }
  257. this.layoutEngine.positionInitially(newNodes);
  258. if (doNotEmit === false) {
  259. this.body.emitter.emit("_dataChanged");
  260. }
  261. }
  262. /**
  263. * Update existing nodes, or create them when not yet existing
  264. * @param {number[] | string[]} ids id's of changed nodes
  265. * @param {Array} changedData array with changed data
  266. * @param {Array|undefined} oldData optional; array with previous data
  267. * @private
  268. */
  269. update(ids, changedData, oldData) {
  270. let nodes = this.body.nodes;
  271. let dataChanged = false;
  272. for (let i = 0; i < ids.length; i++) {
  273. let id = ids[i];
  274. let node = nodes[id];
  275. let data = changedData[i];
  276. if (node !== undefined) {
  277. // update node
  278. if (node.setOptions(data)) {
  279. dataChanged = true;
  280. }
  281. }
  282. else {
  283. dataChanged = true;
  284. // create node
  285. node = this.create(data);
  286. nodes[id] = node;
  287. }
  288. }
  289. if (!dataChanged && oldData !== undefined) {
  290. // Check for any changes which should trigger a layout recalculation
  291. // For now, this is just 'level' for hierarchical layout
  292. // Assumption: old and new data arranged in same order; at time of writing, this holds.
  293. dataChanged = changedData.some(function(newValue, index) {
  294. let oldValue = oldData[index];
  295. return (oldValue && oldValue.level !== newValue.level);
  296. });
  297. }
  298. if (dataChanged === true) {
  299. this.body.emitter.emit("_dataChanged");
  300. }
  301. else {
  302. this.body.emitter.emit("_dataUpdated");
  303. }
  304. }
  305. /**
  306. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  307. * @param {number[] | string[]} ids
  308. * @private
  309. */
  310. remove(ids) {
  311. let nodes = this.body.nodes;
  312. for (let i = 0; i < ids.length; i++) {
  313. let id = ids[i];
  314. delete nodes[id];
  315. }
  316. this.body.emitter.emit("_dataChanged");
  317. }
  318. /**
  319. * create a node
  320. * @param {Object} properties
  321. * @param {class} [constructorClass=Node.default]
  322. * @returns {*}
  323. */
  324. create(properties, constructorClass = Node) {
  325. return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions)
  326. }
  327. /**
  328. *
  329. * @param {boolean} [clearPositions=false]
  330. */
  331. refresh(clearPositions = false) {
  332. util.forEach(this.body.nodes, (node, nodeId) => {
  333. let data = this.body.data.nodes.get(nodeId);
  334. if (data !== undefined) {
  335. if (clearPositions === true) {
  336. node.setOptions({x:null, y:null});
  337. }
  338. node.setOptions({ fixed: false });
  339. node.setOptions(data);
  340. }
  341. });
  342. }
  343. /**
  344. * Returns the positions of the nodes.
  345. * @param {Array.<Node.id>|String} [ids] --> optional, can be array of nodeIds, can be string
  346. * @returns {{}}
  347. */
  348. getPositions(ids) {
  349. let dataArray = {};
  350. if (ids !== undefined) {
  351. if (Array.isArray(ids) === true) {
  352. for (let i = 0; i < ids.length; i++) {
  353. if (this.body.nodes[ids[i]] !== undefined) {
  354. let node = this.body.nodes[ids[i]];
  355. dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  356. }
  357. }
  358. }
  359. else {
  360. if (this.body.nodes[ids] !== undefined) {
  361. let node = this.body.nodes[ids];
  362. dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
  363. }
  364. }
  365. }
  366. else {
  367. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  368. let node = this.body.nodes[this.body.nodeIndices[i]];
  369. dataArray[this.body.nodeIndices[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  370. }
  371. }
  372. return dataArray;
  373. }
  374. /**
  375. * Load the XY positions of the nodes into the dataset.
  376. */
  377. storePositions() {
  378. // todo: add support for clusters and hierarchical.
  379. let dataArray = [];
  380. var dataset = this.body.data.nodes.getDataSet();
  381. for (let nodeId in dataset._data) {
  382. if (dataset._data.hasOwnProperty(nodeId)) {
  383. let node = this.body.nodes[nodeId];
  384. if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
  385. dataArray.push({ id: node.id, x: Math.round(node.x), y: Math.round(node.y) });
  386. }
  387. }
  388. }
  389. dataset.update(dataArray);
  390. }
  391. /**
  392. * get the bounding box of a node.
  393. * @param {Node.id} nodeId
  394. * @returns {j|*}
  395. */
  396. getBoundingBox(nodeId) {
  397. if (this.body.nodes[nodeId] !== undefined) {
  398. return this.body.nodes[nodeId].shape.boundingBox;
  399. }
  400. }
  401. /**
  402. * Get the Ids of nodes connected to this node.
  403. * @param {Node.id} nodeId
  404. * @param {'to'|'from'|undefined} direction values 'from' and 'to' select respectively parent and child nodes only.
  405. * Any other value returns both parent and child nodes.
  406. * @returns {Array}
  407. */
  408. getConnectedNodes(nodeId, direction) {
  409. let nodeList = [];
  410. if (this.body.nodes[nodeId] !== undefined) {
  411. let node = this.body.nodes[nodeId];
  412. let nodeObj = {}; // used to quickly check if node already exists
  413. for (let i = 0; i < node.edges.length; i++) {
  414. let edge = node.edges[i];
  415. if (direction !== 'to' && edge.toId == node.id) { // these are double equals since ids can be numeric or string
  416. if (nodeObj[edge.fromId] === undefined) {
  417. nodeList.push(edge.fromId);
  418. nodeObj[edge.fromId] = true;
  419. }
  420. }
  421. else if (direction !== 'from' && edge.fromId == node.id) { // these are double equals since ids can be numeric or string
  422. if (nodeObj[edge.toId] === undefined) {
  423. nodeList.push(edge.toId);
  424. nodeObj[edge.toId] = true;
  425. }
  426. }
  427. }
  428. }
  429. return nodeList;
  430. }
  431. /**
  432. * Get the ids of the edges connected to this node.
  433. * @param {Node.id} nodeId
  434. * @returns {*}
  435. */
  436. getConnectedEdges(nodeId) {
  437. let edgeList = [];
  438. if (this.body.nodes[nodeId] !== undefined) {
  439. let node = this.body.nodes[nodeId];
  440. for (let i = 0; i < node.edges.length; i++) {
  441. edgeList.push(node.edges[i].id)
  442. }
  443. }
  444. else {
  445. console.log("NodeId provided for getConnectedEdges does not exist. Provided: ", nodeId);
  446. }
  447. return edgeList;
  448. }
  449. /**
  450. * Move a node.
  451. *
  452. * @param {Node.id} nodeId
  453. * @param {number} x
  454. * @param {number} y
  455. */
  456. moveNode(nodeId, x, y) {
  457. if (this.body.nodes[nodeId] !== undefined) {
  458. this.body.nodes[nodeId].x = Number(x);
  459. this.body.nodes[nodeId].y = Number(y);
  460. setTimeout(() => {this.body.emitter.emit("startSimulation")},0);
  461. }
  462. else {
  463. console.log("Node id supplied to moveNode does not exist. Provided: ", nodeId);
  464. }
  465. }
  466. }
  467. export default NodesHandler;