tree.js

  1. var TreeNode = require('./tree-node');
  2. var Traverser = require('./traverser');
  3. module.exports = (function(){
  4. // Flag bad practises
  5. 'use strict';
  6. // ------------------------------------
  7. // Basic Setup
  8. // ------------------------------------
  9. /**
  10. * @class Tree
  11. * @classdesc Represents the tree in which data nodes can be inserted
  12. * @constructor
  13. */
  14. function Tree(){
  15. /**
  16. * Represents the root node of the tree.
  17. *
  18. * @member
  19. * @type {object}
  20. * @default "null"
  21. */
  22. this._rootNode = null;
  23. /**
  24. * Represents the current node in question. `_currentNode` points to most recent
  25. * node inserted or parent node of most recent node removed.
  26. *
  27. * @member
  28. * @memberof Tree.
  29. * @type {object}
  30. * @default "null"
  31. */
  32. this._currentNode = null;
  33. /**
  34. * Represents the traverser which search/traverse a tree in DFS and BFS fashion.
  35. *
  36. * @member
  37. * @memberof Tree
  38. * @type {object}
  39. * @instance
  40. * @default {@link Traverser}
  41. */
  42. this._traverser = new Traverser(this);
  43. }
  44. // ------------------------------------
  45. // Getters and Setters
  46. // ------------------------------------
  47. /**
  48. * Returns a root node of the tree.
  49. *
  50. * @method rootNode
  51. * @memberof Tree
  52. * @instance
  53. * @return {TreeNode} - root node of the tree.
  54. */
  55. Tree.prototype.rootNode = function(){
  56. return this._rootNode;
  57. };
  58. /**
  59. * Returns a current node in a tree
  60. *
  61. * @method currentNode
  62. * @memberof Tree
  63. * @instance
  64. * @return {TreeNode} - current node of the tree.
  65. */
  66. Tree.prototype.currentNode = function(){
  67. return this._currentNode;
  68. };
  69. /**
  70. * Getter function that returns {@link Traverser}.
  71. *
  72. * @method traverser
  73. * @memberof Tree
  74. * @instance
  75. * @return {@link Traverser} for the tree.
  76. */
  77. Tree.prototype.traverser = function(){
  78. return this._traverser;
  79. };
  80. // ------------------------------------
  81. // Methods
  82. // ------------------------------------
  83. /**
  84. * Checks whether tree is empty.
  85. *
  86. * @method isEmpty
  87. * @memberof Tree
  88. * @instance
  89. * @return {boolean} whether tree is empty.
  90. */
  91. Tree.prototype.isEmpty = function(){
  92. return this._rootNode === null && this._currentNode === null;
  93. };
  94. /**
  95. * Empties the tree. Removes all nodes from tree.
  96. *
  97. * @method pruneAllNodes
  98. * @memberof Tree
  99. * @instance
  100. * @return {@link Tree} empty tree.
  101. */
  102. Tree.prototype.pruneAllNodes = function(){
  103. if(this._rootNode && this._currentNode) this.trimBranchFrom(this._rootNode);
  104. return this;
  105. };
  106. /**
  107. * Creates a {@link TreeNode} that contains the data provided and insert it in a tree.
  108. * New node gets inserted to the `_currentNode` which updates itself upon every insertion and deletion.
  109. *
  110. * @method insert
  111. * @memberof Tree
  112. * @instance
  113. * @param {object} data - data that has to be stored in tree-node.
  114. * @return {object} - instance of {@link TreeNode} that represents node inserted.
  115. * @example
  116. *
  117. * // Insert single value
  118. * tree.insert(183);
  119. *
  120. * // Insert array of values
  121. * tree.insert([34, 565, 78]);
  122. *
  123. * // Insert complex data
  124. * tree.insert({
  125. * key: '#berries',
  126. * value: { name: 'Apple', color: 'Red'}
  127. * });
  128. */
  129. Tree.prototype.insert = function(data){
  130. var node = new TreeNode(data);
  131. if(this._rootNode === null && this._currentNode === null){
  132. node._depth = 1;
  133. this._rootNode = this._currentNode = node;
  134. } else {
  135. node._parentNode = this._currentNode;
  136. this._currentNode._childNodes.push(node);
  137. this._currentNode = node;
  138. node.depth = node._parentNode._depth + 1;
  139. }
  140. return node;
  141. };
  142. /**
  143. * Removes a node from tree and updates `_currentNode` to parent node of node removed.
  144. *
  145. * @method remove
  146. * @memberof Tree
  147. * @instance
  148. * @param {object} node - {@link TreeNode} that has to be removed.
  149. * @param {boolean} trim - indicates whether to remove entire branch from the specified node.
  150. */
  151. Tree.prototype.remove = function(node, trim){
  152. if(trim || node === this._rootNode){
  153. // Trim Entire branch
  154. this.trimBranchFrom(node);
  155. } else {
  156. // Upate children's parent to grandparent
  157. node._childNodes.forEach(function(_child){
  158. _child._parentNode = node._parentNode;
  159. node._parentNode._childNodes.push(_child);
  160. });
  161. // Delete itslef from parent child array
  162. node._parentNode._childNodes.splice(node._parentNode._childNodes.indexOf(node), 1);
  163. // Update Current Node
  164. this._currentNode = node._parentNode;
  165. // Clear Child Array
  166. node._childNodes = [];
  167. node._parentNode = null;
  168. node._data = null;
  169. }
  170. };
  171. /**
  172. * Remove an entire branch starting with specified node.
  173. *
  174. * @method trimBranchFrom
  175. * @memberof Tree
  176. * @instance
  177. * @param {object} node - {@link TreeNode} from which entire branch has to be removed.
  178. */
  179. Tree.prototype.trimBranchFrom = function(node){
  180. // Hold `this`
  181. var thiss = this;
  182. // trim brach recursively
  183. (function recur(node){
  184. node._childNodes.forEach(recur);
  185. node._childNodes = [];
  186. node._data = null;
  187. }(node));
  188. // Update Current Node
  189. if(node._parentNode){
  190. node._parentNode._childNodes.splice(node._parentNode._childNodes.indexOf(node), 1);
  191. thiss._currentNode = node._parentNode;
  192. } else {
  193. thiss._rootNode = thiss._currentNode = null;
  194. }
  195. };
  196. /**
  197. * Inserts node to a particular node present in the tree. Particular node here is searched
  198. * in the tree based on the criteria provided.
  199. *
  200. * @method insertTo
  201. * @memberof Tree
  202. * @instance
  203. * @param {function} criteria - Callback function that specifies the search criteria
  204. * for node to which new node is to be inserted. Criteria callback here receives {@link TreeNode#_data}
  205. * in parameter and MUST return boolean indicating whether that data satisfies your criteria.
  206. * @param {object} data - that has to be stored in tree-node.
  207. * @return {object} - instance of {@link TreeNode} that represents node inserted.
  208. * @example
  209. *
  210. * // Insert data
  211. * tree.insert({
  212. * key: '#apple',
  213. * value: { name: 'Apple', color: 'Red'}
  214. * });
  215. *
  216. * // New Data
  217. * var greenApple = {
  218. * key: '#greenapple',
  219. * value: { name: 'Green Apple', color: 'Green' }
  220. * };
  221. *
  222. * // Insert data to node which has `key` = #apple
  223. * tree.insertTo(function(data){
  224. * return data.key === '#apple'
  225. * }, greenApple);
  226. */
  227. Tree.prototype.insertTo = function(criteria, data){
  228. var node = this.traverser().searchDFS(criteria);
  229. return this.insertToNode(node, data);
  230. };
  231. /**
  232. * Inserts node to a particular node present in the tree. Particular node here is an instance of {@link TreeNode}
  233. *
  234. * @method insertToNode
  235. * @memberof Tree
  236. * @instance
  237. * @param {function} node - {@link TreeNode} to which data node is to be inserted.
  238. * @param {object} data - that has to be stored in tree-node.
  239. * @return {object} - instance of {@link TreeNode} that represents node inserted.
  240. * @example
  241. *
  242. * // Insert data
  243. * var node = tree.insert({
  244. * key: '#apple',
  245. * value: { name: 'Apple', color: 'Red'}
  246. * });
  247. *
  248. * // New Data
  249. * var greenApple = {
  250. * key: '#greenapple',
  251. * value: { name: 'Green Apple', color: 'Green' }
  252. * };
  253. *
  254. * // Insert data to node
  255. * tree.insertToNode(node, greenApple);
  256. */
  257. Tree.prototype.insertToNode = function(node, data){
  258. var newNode = new TreeNode(data);
  259. newNode._parentNode = node;
  260. newNode._depth = newNode._parentNode._depth + 1;
  261. node._childNodes.push(newNode);
  262. this._currentNode = newNode;
  263. return newNode;
  264. };
  265. /**
  266. * Finds a distance between two nodes
  267. *
  268. * @method distanceBetween
  269. * @memberof Tree
  270. * @instance
  271. * @param {@link TreeNode} fromNode - Node from which distance is to be calculated
  272. * @param {@link TreeNode} toNode - Node to which distance is to be calculated
  273. * @return {Number} - distance(number of hops) between two nodes.
  274. */
  275. Tree.prototype.distanceBetween = function(fromNode, toNode){
  276. return fromNode.distanceToRoot() + toNode.distanceToRoot() - 2 * this.findCommonParent(fromNode, toNode).distanceToRoot();
  277. };
  278. /**
  279. * Finds a common parent between nodes
  280. *
  281. * @method findCommonParent
  282. * @memberof Tree
  283. * @instance
  284. * @param {@link TreeNode} fromNode
  285. * @param {@link TreeNode} toNode
  286. * @return {@link TreeNode} - common parent
  287. */
  288. Tree.prototype.findCommonParent = function(fromNode, toNode){
  289. // Get ancestory of both nodes
  290. var fromNodeAncestors = fromNode.getAncestry();
  291. var toNodeAncestors = toNode.getAncestry();
  292. // Find Commont
  293. var common = null;
  294. fromNodeAncestors.some(function(ancestor){
  295. if(toNodeAncestors.indexOf(ancestor) !== -1){
  296. common = ancestor;
  297. return true;
  298. }
  299. });
  300. // Return Common
  301. return common;
  302. };
  303. /**
  304. * Exports the tree data in format specified. It maintains herirachy by adding
  305. * additional "children" property to returned value of `criteria` callback.
  306. *
  307. * @method export
  308. * @memberof Tree
  309. * @instance
  310. * @param {Tree~criteria} criteria - Callback function that receives data in parameter
  311. * and MUST return a formatted data that has to be exported. A new property "children" is added to object returned
  312. * that maintains the heirarchy of nodes.
  313. * @return {object} - {@link TreeNode}.
  314. * @example
  315. *
  316. * var rootNode = tree.insert({
  317. * key: '#apple',
  318. * value: { name: 'Apple', color: 'Red'}
  319. * });
  320. *
  321. * tree.insert({
  322. * key: '#greenapple',
  323. * value: { name: 'Green Apple', color: 'Green'}
  324. * });
  325. *
  326. * tree.insertToNode(rootNode, {
  327. * key: '#someanotherapple',
  328. * value: { name: 'Some Apple', color: 'Some Color' }
  329. * });
  330. *
  331. * // Export the tree
  332. * var exported = tree.export(function(data){
  333. * return { name: data.value.name };
  334. * });
  335. *
  336. * // Result in `exported`
  337. * {
  338. * "name": "Apple",
  339. * "children": [
  340. *   {
  341. *     "name": "Green Apple",
  342. *     "children": []
  343. *   },
  344. *   {
  345. *     "name": "Some Apple",
  346. *     "children": []
  347. *  }
  348. * ]
  349. *}
  350. *
  351. */
  352. Tree.prototype.export = function(criteria){
  353. // Check if rootNode is not null
  354. if(!this._rootNode){
  355. return null;
  356. }
  357. return this._rootNode.export(criteria);
  358. };
  359. /**
  360. * Returns a new compressed tree. While compressing it considers nodes that
  361. * satisfies given criteria and skips the rest of the nodes, making tree compressed.
  362. *
  363. * @method compress
  364. * @memberof Tree
  365. * @instance
  366. * @param {Tree~criteria} criteria - Callback function that checks whether node satifies certain criteria. MUST return boolean.
  367. * @return {@link Tree} - A new compressed tree.
  368. */
  369. Tree.prototype.compress = function(criteria){
  370. // Check if criteria is specified
  371. if(!criteria || typeof criteria !== 'function')
  372. throw new Error('Compress criteria not specified');
  373. // Check if tree is not empty
  374. if(this.isEmpty()){
  375. return null;
  376. }
  377. // Create New Tree
  378. var tree = new Tree();
  379. // Hold `this`
  380. var thiss = this;
  381. // Recur DFS
  382. (function recur(node, parent){
  383. // Check-in
  384. var checkIn = thiss.rootNode() === node || node.matchCriteria(criteria);
  385. // Check if checked-in
  386. if(checkIn){
  387. if(tree.isEmpty()){
  388. parent = tree.insert(node.data());
  389. } else {
  390. parent = tree.insertToNode(parent, node.data());
  391. }
  392. } else {
  393. parent._data.hasCompressedNodes = true;
  394. }
  395. // For all child nodes
  396. node.childNodes().forEach(function(_child){
  397. recur(_child, parent);
  398. });
  399. }(this.rootNode(), null));
  400. return tree;
  401. };
  402. /**
  403. * Imports the JSON data into a tree using the criteria provided.
  404. * A property indicating the nesting of object must be specified.
  405. *
  406. * @method import
  407. * @memberof Tree
  408. * @instance
  409. * @param {object} data - JSON data that has be imported
  410. * @param {string} childProperty - Name of the property that holds the nested data.
  411. * @param {Tree~criteria} criteria - Callback function that receives data in parameter
  412. * and MUST return a formatted data that has to be imported in a tree.
  413. * @return {object} - {@link Tree}.
  414. * @example
  415. *
  416. * var data = {
  417. * "trailId": "h2e67d4ea-f85f40e2ae4a06f4777864de",
  418. * "initiatedAt": 1448393492488,
  419. * "snapshots": {
  420. * "snapshotId": "b3d132131-213c20f156339ea7bdcb6273",
  421. * "capturedAt": 1448393495353,
  422. * "thumbnail": "data:img",
  423. * "children": [
  424. * {
  425. * "snapshotId": "yeb7ab27c-b36ff1b04aefafa9661243de",
  426. * "capturedAt": 1448393499685,
  427. * "thumbnail": "data:image/",
  428. * "children": [
  429. * {
  430. * "snapshotId": "a00c9828f-e2be0fc4732f56471e77947a",
  431. * "capturedAt": 1448393503061,
  432. * "thumbnail": "data:image/png;base64",
  433. * "children": []
  434. * }
  435. * ]
  436. * }
  437. * ]
  438. * }
  439. * };
  440. *
  441. * // Import
  442. * // This will result in a tree having nodes containing `id` and `thumbnail` as data
  443. * tree.import(data, 'children', function(nodeData){
  444. * return {
  445. * id: nodeData.snapshotId,
  446. * thumbnail: nodeData.thumbnail
  447. * }
  448. * });
  449. *
  450. */
  451. Tree.prototype.import = function(data, childProperty, criteria){
  452. // Empty all tree
  453. if(this._rootNode) this.trimBranchFrom(this._rootNode);
  454. // Set Current Node to root node as null
  455. this._currentNode = this._rootNode = null;
  456. // Hold `this`
  457. var thiss = this;
  458. // Import recursively
  459. (function importRecur(node, recurData){
  460. // Format data from given criteria
  461. var _data = criteria(recurData);
  462. // Create Root Node
  463. if(!node){
  464. node = thiss.insert(_data);
  465. } else {
  466. node = thiss.insertToNode(node, _data);
  467. }
  468. // For Every Child
  469. recurData[childProperty].forEach(function(_child){
  470. importRecur(node, _child);
  471. });
  472. }(this._rootNode, data));
  473. // Set Current Node to root node
  474. this._currentNode = this._rootNode;
  475. return this;
  476. };
  477. /**
  478. * Callback that receives a node data in parameter and expects user to return one of following:
  479. * 1. {@link Traverser#searchBFS} - {boolean} in return indicating whether given node satisfies criteria.
  480. * 2. {@link Traverser#searchDFS} - {boolean} in return indicating whether given node satisfies criteria.
  481. * 3. {@link Tree#export} - {object} in return indicating formatted data object.
  482. * @callback criteria
  483. * @param data {object} - data of particular {@link TreeNode}
  484. */
  485. // ------------------------------------
  486. // Export
  487. // ------------------------------------
  488. return Tree;
  489. }());