Source for file Tree.class.php

Documentation is available at Tree.class.php

  1. <?php
  2.  
  3. require_once(HARMONI."oki2/hierarchy/tree/Tree.interface.php");
  4. require_once(HARMONI."oki2/hierarchy/tree/TreeNode.class.php");
  5.  
  6. /**
  7. * The Tree data structure used by the Hierarchy.
  8. *
  9. * @package harmoni.osid_v2.hierarchy.tree
  10. *
  11. * @copyright Copyright &copy; 2005, Middlebury College
  12. * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License (GPL)
  13. *
  14. * @version $Id: Tree.class.php,v 1.16 2007/09/04 20:25:42 adamfranco Exp $
  15. * @since Created: 8/30/2003
  16. */
  17. class Tree extends TreeInterface {
  18.  
  19.  
  20.  
  21. /**
  22. * The nodes of this tree. This is an array of node object. The indices of
  23. * the array correspond to the node id.
  24. * @var array _nodes
  25. * @access private
  26. */
  27. var $_nodes;
  28. /**
  29. * The size of this tree.
  30. * @var integer _size
  31. * @access private
  32. */
  33. var $_size;
  34.  
  35. /**
  36. * The constructor does some initializations.
  37. * @access public
  38. */
  39. function Tree() {
  40. $this->_nodes = array();
  41. $this->_size = 0;
  42. $this->_traversalCache = array();
  43. }
  44. /**
  45. * Adds the specified node to the tree and makes it a child of the specified
  46. * parent. If the parent is not specified, then it makes the node a root. Always
  47. * use this method instead of the addChild() method of the individual tree nodes.
  48. * @access public
  49. * @param ref object The node to add.
  50. * @param optional ref object parent The node that will become the parent of the added node.
  51. * @param optional boolean $clearTraversal If true, the tree will clear its
  52. * traversal cache. This is needed when changing parentage.
  53. * @return void
  54. */
  55. function addNode($node, $parent, $clearTraversal = false ) {
  56. // ** parameter validation
  57. $extendsRule = ExtendsValidatorRule::getRule("TreeNode");
  58. $optionalRule = OptionalRule::getRule($extendsRule);
  59. ArgumentValidator::validate($node, $extendsRule, true);
  60. ArgumentValidator::validate($parent, $optionalRule, true);
  61. // ** end of parameter validation
  62. $id = $node->getId();
  63.  
  64. // if node has not been cached then do so
  65. if (!$this->nodeExists($id)) {
  66. // add the node
  67. $this->_nodes[$id] =$node;
  68. $this->_size++;
  69. }
  70.  
  71. // if $parent is not null, i.e. $node will not be a root
  72. if (!is_null($parent)) {
  73. // make sure $parent is in the tree
  74. if (!$this->nodeExists($parent->getId())) {
  75. $str = "Attempted to create a child for a node that is not in the tree.";
  76. throwError(new Error($str, "Hierarchy", true));
  77. }
  78. // now add $node as a child to $parent
  79. $parent->addChild($node);
  80. if ($clearTraversal) {
  81. // clear the traversal caches as all ancestors of the parent will have
  82. // to have their traverse-down caches rebuilt and the child and its
  83. // decendents will all need their traverse-up caches rebuilt.
  84. // $this->clearTraverseUpCaches($node);
  85. // $this->clearTraverseDownCaches($parent);
  86.  
  87. // It appears that selectively clearing the caches is taking
  88. // significantly longer to do than just resetting the traversal
  89. // caches for all nodes and rebuilding them as needed.
  90. // In my tests on 700-Asset Repositories, the following load times
  91. // (when checking AZs) were found:
  92. // 31.6s clearing all
  93. // 32.8s clearing selective
  94. // When no AZs were checked, the following load times were found:
  95. // 16.5s clearing all
  96. // 25.9s clearing selective
  97. $this->_traversalCache = array();
  98. }
  99. }
  100. }
  101.  
  102. /**
  103. * Delete the node from the tree. This can only be done if the node has no
  104. * parents and no children.
  105. * @access public
  106. * @param object node The node to delete.
  107. * @return void
  108. ***/
  109. function deleteNode($node, $clearTraversal = false) {
  110. // ** parameter validation
  111. $extendsRule = ExtendsValidatorRule::getRule("TreeNode");
  112. ArgumentValidator::validate($node, $extendsRule, true);
  113. // ** end of parameter validation
  114. if (($node->getParentsCount() != 0) || ($node->getChildrenCount() != 0)) {
  115. $str = "Cannot delete a node that has parents or children.";
  116. throwError(new Error($str, "Hierarchy", true));
  117. }
  118. // now delete it
  119. unset($this->_nodes[$node->_id]);
  120. $node = null;
  121. if ($clearTraversal) {
  122. // clear the traversal caches as all ancestors of the parent will have
  123. // to have their traverse-down caches rebuilt and the child and its
  124. // decendents will all need their traverse-up caches rebuilt.
  125. // $this->clearTraverseUpCaches($node);
  126. // $this->clearTraverseDownCaches($parent);
  127.  
  128. // It appears that selectively clearing the caches is taking
  129. // significantly longer to do than just resetting the traversal
  130. // caches for all nodes and rebuilding them as needed.
  131. // In my tests on 700-Asset Repositories, the following load times
  132. // (when checking AZs) were found:
  133. // 31.6s clearing all
  134. // 32.8s clearing selective
  135. // When no AZs were checked, the following load times were found:
  136. // 16.5s clearing all
  137. // 25.9s clearing selective
  138. $this->_traversalCache = array();
  139. }
  140. }
  141.  
  142.  
  143. /**
  144. * Returns the size (number of nodes) in this tree.
  145. * @access public
  146. * @return integer The size (number of nodes) in this tree.
  147. */
  148. function getSize() {
  149. return $this->_size;
  150. }
  151.  
  152.  
  153. /**
  154. * Returns the node with the specified id. If it does not exist, return <code>null</code>.
  155. * @access public
  156. * @param mixed id The id of the requested node.
  157. * @return ref object The requested node. <code>Null</code>, if the node
  158. * is not in the tree.
  159. */
  160. function getNode($id) {
  161. // ** parameter validation
  162. ArgumentValidator::validate($id,
  163. OrValidatorRule::getRule(
  164. NonzeroLengthStringValidatorRule::getRule(),
  165. IntegerValidatorRule::getRule()),
  166. true);
  167. // ** end of parameter validation
  168. return $this->_nodes[$id];
  169. }
  170. /**
  171. * Returns <code>true</code> if the node with the specified id (string) exists.
  172. * @access public
  173. * @param mixed id The id of the node.
  174. * @return boolean <code>true</code> if the node with the specified id is in the tree; else <code>false</code>.
  175. */
  176. function nodeExists($id) {
  177. // ** parameter validation
  178. ArgumentValidator::validate($id,
  179. OrValidatorRule::getRule(
  180. NonzeroLengthStringValidatorRule::getRule(),
  181. IntegerValidatorRule::getRule()),
  182. true);
  183. // ** end of parameter validation
  184. return isset($this->_nodes[$id]);
  185. }
  186. /**
  187. * Simply returns all nodes of this tree in an array in no particular
  188. * order.
  189. * @access public
  190. * @return ref array An array of all nodes.
  191. */
  192. function getNodes() {
  193. return $this->_nodes;
  194. }
  195.  
  196. /**
  197. * Traverses the tree and returns all the nodes in an array. The traversal
  198. * is a depth-first pre-order traversal starting from the specified node.
  199. * @access public
  200. * @param ref object node The node to start traversal from.
  201. * @param boolean down If <code>true</code>, this argument specifies that the traversal will
  202. * go down the children; if <code>false</code> then it will go up the parents.
  203. * @param integer levels Specifies how many levels of nodes remain to be fetched. This
  204. * will be recursively decremented and at 0 the recursion will stop. If this is negative
  205. * then the recursion will go on until the last level is processed.
  206. * @return ref array An array of all nodes in the tree visited in a pre-order
  207. * manner. The keys of the array correspond to the nodes' ids.
  208. * Each element of the array is another array of two elements, the first
  209. * being the node itself, and the second being the depth of the node relative
  210. * to the starting node. Descendants are assigned increasingly positive levels;
  211. * ancestors increasingly negative levels.
  212. */
  213. function traverse($node, $down, $levels) {
  214. // ** parameter validation
  215. $extendsRule = ExtendsValidatorRule::getRule("TreeNodeInterface");
  216. ArgumentValidator::validate($node, $extendsRule, true);
  217. ArgumentValidator::validate($down, BooleanValidatorRule::getRule(), true);
  218. ArgumentValidator::validate($levels, IntegerValidatorRule::getRule(), true);
  219. // ** end of parameter validation
  220. if (!$this->nodeExists($node->getId())) {
  221. $str = "Attempted to traverse from a node that does not exist in the tree.";
  222. throwError(new Error($str, "Hierarchy", true));
  223. }
  224. $cacheKey = $this->getCacheKey($node, $down, $levels);
  225. if (!isset($this->_traversalCache[$cacheKey])) {
  226. $this->_traversalCache[$cacheKey] = array();
  227.  
  228. $this->_traverse($this->_traversalCache[$cacheKey], $node, $down, $levels, $levels);
  229. }
  230. return $this->_traversalCache[$cacheKey];
  231. }
  232. /**
  233. * Clear the traverse up caches for the given node and all of its decendents
  234. *
  235. * @param object Node $node
  236. * @return void
  237. * @access public
  238. * @since 11/9/05
  239. */
  240. function clearTraverseUpCaches ( $node ) {
  241. $treeNodes =$this->traverse($node, false, -1);
  242. foreach (array_keys($treeNodes) as $i => $key) {
  243. $decendentNode =$this->getNode($key);
  244. $this->clearTraversalCaches($decendentNode, false);
  245. }
  246. }
  247. /**
  248. * Clear the traverse down caches for the given node and all of its ancestors
  249. *
  250. * @param object Node $node
  251. * @return void
  252. * @access public
  253. * @since 11/9/05
  254. */
  255. function clearTraverseDownCaches ( $node ) {
  256. $treeNodes =$this->traverse($node, true, -1);
  257. foreach (array_keys($treeNodes) as $i => $key) {
  258. $ancestorNode =$this->getNode($key);
  259. $this->clearTraversalCaches($ancestorNode, true);
  260. }
  261. }
  262. /**
  263. * Clear the traverse caches for the given node and direction
  264. *
  265. * @param object Node $node
  266. * @param boolean $down
  267. * @return void
  268. * @access public
  269. * @since 11/9/05
  270. */
  271. function clearTraversalCaches ( $node, $down ) {
  272. $regex = "/^".$this->getCacheKey($node, $down, '[\-0-9]+')."$/";
  273. foreach(array_keys($this->_traversalCache) as $cacheKey) {
  274. if (preg_match($regex, $cacheKey)) {
  275. // printpre("removing traversal cache: $cacheKey");
  276. unset($this->_traversalCache[$cacheKey]);
  277. }
  278. }
  279. }
  280. /**
  281. * Answer the traversal cacheKey for this node, direction, and levels
  282. *
  283. * @param object Node $node
  284. * @param boolean $down
  285. * @param integer $levels
  286. * @return string
  287. * @access public
  288. * @since 11/9/05
  289. */
  290. function getCacheKey ( $node, $down, $levels ) {
  291. return $node->getId()."::".(($down)?"TRUE":"FALSE")."::".$levels;
  292. }
  293. /**
  294. * A private recursive function that performs a depth-first pre-order traversal.
  295. * @access private
  296. * @param ref array The array where to store the result. Will consists of all
  297. * nodes in the tree visited in a pre-order manner.
  298. * @param ref object node The node to be visited.
  299. * @param boolean down If <code>true</code>, this argument specifies that the traversal will
  300. * go down the children; if <code>false</code> then it will go up the parents.
  301. * @param integer levels Specifies how many levels of nodes remain to be fetched. This
  302. * will be recursively decremented and at 0 the recursion will stop. If this is negative
  303. * then the recursion will go on until the last level is processed.
  304. * @param integer startingLevels This is the original value of the levels. This
  305. * is needed in order to properly calculate the relative depth of each returned node.
  306. * @param string $initiatingNodeId The node that initiated the traversal to this node
  307. * @return ref array An array of all nodes in the tree visited in a pre-order
  308. * manner. The keys of the array correspond to the nodes' ids.
  309. * Each element of the array is another array of two elements, the first
  310. * being the node itself, and the second being the depth of the node relative
  311. * to the starting node. Descendants are assigned increasingly positive levels;
  312. * ancestors increasingly negative levels.
  313. */
  314. function _traverse( &$result, $node, $down, $levels, $startingLevel, $initiatingNodeId = null) {
  315. // visit the node
  316. // note: the node could possibly been have visited already (if it has
  317. // several parents and we reached it earlier through a different parent);
  318. // in this case, we would only change the depth if it got smaller
  319. $mult = ($down) ? 1 : -1;
  320. $newLevel = ($startingLevel - $levels) * $mult;
  321. if (!isset($result[$node->getId()])) {
  322. $result[$node->getId()][0] =$node;
  323. $result[$node->getId()][1] = $newLevel;
  324. }
  325. else if (abs($result[$node->getId()][1]) > abs($newLevel))
  326. $result[$node->getId()][1] = $newLevel;
  327. // Record which parents and children we have found to allow the hierarchy
  328. // cache to build and store alternative views into the hierarchy for quicker
  329. // retreival.
  330. if (!is_null($initiatingNodeId)) {
  331. if ($down) {
  332. if (!isset($result[$node->getId()]['parents']))
  333. $result[$node->getId()]['parents'] = array();
  334. if (!in_array($initiatingNodeId, $result[$node->getId()]['parents']))
  335. $result[$node->getId()]['parents'][] = $initiatingNodeId;
  336. } else {
  337. if (!isset($result[$node->getId()]['children']))
  338. $result[$node->getId()]['children'] = array();
  339. if (!in_array($initiatingNodeId, $result[$node->getId()]['children']))
  340. $result[$node->getId()]['children'][] = $initiatingNodeId;
  341. }
  342. }
  343. // base case 1 : all levels have been processed
  344. if ($levels == 0)
  345. return;
  346. // base case 2: there are no more nodes left
  347. if ($down)
  348. $noMoreLeft = !$node->hasChildren();
  349. else
  350. $noMoreLeft = !$node->hasParents();
  351. if ($noMoreLeft)
  352. return;
  353.  
  354. // visit the children/parents
  355. if ($down)
  356. $nodes =$node->getChildren();
  357. else
  358. $nodes =$node->getParents();
  359. foreach (array_keys($nodes) as $i => $key)
  360. // recurse for each node
  361. $this->_traverse($result, $nodes[$key], $down, $levels - 1, $startingLevel, $node->getId());
  362. }
  363.  
  364. }
  365.  
  366.  
  367. ?>

Documentation generated on Wed, 19 Sep 2007 10:27:32 -0400 by phpDocumentor 1.3.0RC3