Coverage Report - org.jfree.data.hc.HCTreeNode
 
Classes in this File Line Coverage Branch Coverage Complexity
HCTreeNode
95%
89/94
100%
28/28
4.25
 
 1  
 /* ======================================================================= 
 2  
  * A visualisation library extension for JFreeChart. Please see JFreeChart
 3  
  * for further information.
 4  
  * =======================================================================
 5  
  * Copyright (C) 2006  University of Helsinki, Department of Computer Science
 6  
  *
 7  
  * This library is free software; you can redistribute it and/or
 8  
  * modify it under the terms of the GNU Lesser General Public
 9  
  * License as published by the Free Software Foundation; either
 10  
  * version 2.1 of the License, or (at your option) any later version.
 11  
  *
 12  
  * This library is distributed in the hope that it will be useful,
 13  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 15  
  * Lesser General Public License for more details.
 16  
  *
 17  
  * You should have received a copy of the GNU Lesser General Public
 18  
  * License along with this library; if not, write to the Free Software
 19  
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 20  
  * -----------------------------
 21  
  * Contact:  ohtu@cs.helsinki.fi
 22  
  * -----------------------------
 23  
  *
 24  
  */
 25  
 
 26  
 package org.jfree.data.hc;
 27  
 
 28  
 import java.lang.RuntimeException;
 29  
 
 30  
 /**
 31  
 * A class representing single node of a cluster tree. This is
 32  
 * part of {@link HCDataset}.
 33  
 * @author  viski project
 34  
 */
 35  
 public class HCTreeNode {
 36  
 
 37  
     private HCTreeNode leftChild;
 38  
     private HCTreeNode rightChild;
 39  
     private HCTreeNode parent;
 40  
     private DataRange dataRange;
 41  
     private double height;
 42  
     private boolean finalized;
 43  
 
 44  
     /**
 45  
      * Creates a new HCTreeNode. This version of the constructor is
 46  
      * usable for the branch nodes.
 47  
      *
 48  
      * @param height  the height of the node.
 49  
      */
 50  94
     public HCTreeNode(double height) throws IllegalArgumentException {
 51  
 
 52  94
         if (height < 0) throw new IllegalArgumentException("height given to HCTreeNode() was negative.");
 53  94
         this.height = height;
 54  94
         this.dataRange = new DataRange(0,-1); // empty range.
 55  
 
 56  94
     }
 57  
 
 58  
     /**
 59  
      * Creates a new HCTreeNode. This version of the constructor is
 60  
      * usable for the leaf nodes.
 61  
      *
 62  
      * @param height  the height of the node.
 63  
      * @param index  the index of the heatmap row/column this node
 64  
      * corresponds to.
 65  
      */
 66  122
     public HCTreeNode(double height,int index) throws IllegalArgumentException {
 67  
 
 68  122
         this.dataRange = new DataRange(index,index);
 69  
 
 70  122
         this.height = height;
 71  122
         this.finalized = false;
 72  
 
 73  122
     }
 74  
 
 75  
     /**
 76  
      * Checks if this tree is already finalized.
 77  
      *
 78  
      * @throws RuntimeException  if the tree is already finalized.
 79  
      */
 80  
     private void assertFinalized() {
 81  
 
 82  508
         if (this.finalized) throw new RuntimeException();
 83  
 
 84  506
     }
 85  
 
 86  
     /**
 87  
      * Returns the parent of this node.
 88  
      *
 89  
      * @return  parent node, or null, if the node has no parents.
 90  
      */
 91  
     public HCTreeNode getParent() {
 92  
 
 93  12
         return this.parent;
 94  
 
 95  
     }
 96  
 
 97  
     /**
 98  
      * Returns the left child of this node.
 99  
      *
 100  
      * @return  left child node, or null, if the node has no left child.
 101  
      */
 102  
     public HCTreeNode getLeftChild() {
 103  
 
 104  477
         return this.leftChild;
 105  
 
 106  
     }
 107  
 
 108  
     /**
 109  
      * Returns the right child of this node.
 110  
      *
 111  
      * @return  right child node, or null, if the node has no right child.
 112  
      */
 113  
     public HCTreeNode getRightChild() {
 114  
 
 115  505
         return this.rightChild;
 116  
 
 117  
     }
 118  
 
 119  
     /**
 120  
      * Returns the data range of this node and the corresponding subtree.
 121  
      *
 122  
      * @return  a DataRange object.
 123  
      */
 124  
     public DataRange getDataRange() {
 125  
 
 126  553
         return this.dataRange;
 127  
 
 128  
     }
 129  
 
 130  
     /**
 131  
      * Returns the height of this node.
 132  
      *
 133  
      * @return  the height.
 134  
      */
 135  
     public double getHeight() {
 136  
 
 137  400
         return this.height;
 138  
 
 139  
     }
 140  
 
 141  
     /**
 142  
      * Returns the root node of this tree.
 143  
      *
 144  
      * @return  a HCTreeNode object specifying the root node.
 145  
      */
 146  
     public HCTreeNode getRoot() {
 147  
 
 148  
         // nothing checked. hope the tree is not broken.
 149  4
         if (this.parent == null) return this;
 150  2
         else return this.parent.getRoot();
 151  
 
 152  
     }
 153  
 
 154  
     /**
 155  
      * Returns a specified leaf node.
 156  
      *
 157  
      * @param index  data range index that specifies a leaf node.
 158  
      *
 159  
      * @throws IndexOutOfBoundsException, if the specified node
 160  
      * cannot be found.
 161  
      *
 162  
      * @return  the specified HCTreeNode object.
 163  
      */
 164  
     public HCTreeNode getLeafNodeByIndex(int index)
 165  
         throws IndexOutOfBoundsException {
 166  
 
 167  
         // nothing checked. hope the tree is not broken.
 168  
 
 169  13
         if (this.dataRange.contains(index)) {
 170  
 
 171  
             // if this is a leaf node we just return it.
 172  11
             if ((this.leftChild == null) && (this.rightChild == null)) {
 173  
 
 174  5
                 return this;
 175  
 
 176  
             // if it is in left child datarange, we search from there.
 177  6
             } else if ((this.leftChild != null) && 
 178  
                 (this.leftChild.getDataRange().contains(index))) {
 179  3
                     return this.leftChild.getLeafNodeByIndex(index);
 180  
             }
 181  
             // otherwise we search from the right child.
 182  3
             else return this.rightChild.getLeafNodeByIndex(index);
 183  
 
 184  
         } else {
 185  
 
 186  
             // index is not in the range of this node, so
 187  
             // a) it does not exist, or
 188  2
             if (this.parent == null) throw new IndexOutOfBoundsException(
 189  
                     "The specified node does not exist in this tree.");
 190  
             // b) it is a child of grandparents of this node.
 191  1
             return this.parent.getLeafNodeByIndex(index);
 192  
 
 193  
         }
 194  
 
 195  
     }
 196  
 
 197  
     /**
 198  
      * Updates the dataranges of this node and all the affected parents.
 199  
      *
 200  
      * @throws DataRangeMismatchException  if the data ranges of
 201  
      * the children are not next to each other.
 202  
      * @throws RuntimeException  if this node is already finalized.
 203  
      */
 204  
     private void updateDataRange() throws DataRangeMismatchException {
 205  
 
 206  
         DataRange newRange;
 207  
         DataRange oldRange;
 208  
 
 209  172
         assertFinalized();
 210  
 
 211  172
         if (this.leftChild != null)
 212  160
             newRange = (DataRange)this.leftChild.getDataRange().clone();
 213  
         else
 214  12
             newRange = new DataRange(0,-1);
 215  172
         if (this.rightChild != null)
 216  88
             newRange.add(this.rightChild.getDataRange());
 217  
 
 218  172
         oldRange = this.dataRange;
 219  172
         this.dataRange = newRange;
 220  
 
 221  
         // if we changed things, we still need to update parents.
 222  
         // luckily nothing can go wrong with the parents, so we
 223  
         // don't have to worry about undoing changes here.
 224  172
         if ((!newRange.equals(oldRange)) && (this.parent != null))
 225  8
             this.parent.updateDataRange();
 226  172
     }
 227  
 
 228  
     /**
 229  
      * Sets the left child of this node.
 230  
      * This can indirectly affect four nodes (in this order.)
 231  
      * 1. the leftChild pointer is set as asked.
 232  
      * 2. if the new child already had a parent, the pointers of the old parent
 233  
      * will be nulled.
 234  
      * 3. the parent of the new child is set to point to this node.
 235  
      * 4. if this node already had a left child, the parent of the old
 236  
      * child is nulled.
 237  
      * If an exception happens, all changes will be undone.
 238  
      *
 239  
      * @param node  the new child.
 240  
      *
 241  
      * @throws DataRangeMismatchException  if the data range of the new
 242  
      * child is not adjacent to the datarange of the other child.
 243  
      * @throws RuntimeException  if the node is already the right child
 244  
      * of this node.
 245  
      * @throws RuntimeException  if this node is already finalized.
 246  
      */
 247  
     public void setLeftChild(HCTreeNode node) throws DataRangeMismatchException {
 248  
 
 249  
         HCTreeNode oldChild;
 250  
 
 251  86
         assertFinalized(); // may throw.
 252  
 
 253  
         // if this is already the left child, we are done.
 254  
         // We need to return here, to avoid problems later on.
 255  85
         if (this.leftChild == node) return;
 256  
 
 257  
         // if this is already the right child, this is illegal.
 258  84
         if ((node != null) && (this.rightChild == node))
 259  1
             throw new RuntimeException();
 260  
 
 261  83
         oldChild = this.leftChild;
 262  83
         this.leftChild = node;
 263  
 
 264  
         try {
 265  83
             if (node != null) node.setParent(this);
 266  0
         } catch (NotAChildException e) {
 267  
             ; // this never happens.
 268  83
         }
 269  
 
 270  
         try {
 271  83
             if (oldChild != null) oldChild.setParent(null);
 272  0
         } catch (NotAChildException e) {
 273  
             ; // this never happens.
 274  83
         }
 275  
 
 276  83
         this.updateDataRange();
 277  83
     }
 278  
 
 279  
     /**
 280  
      * Sets the left child of this node.
 281  
      * This can indirectly affect four nodes (in this order.)
 282  
      * 1. the leftChild pointer is set as asked.
 283  
      * 2. if the new child already had a parent, the pointers of the old parent
 284  
      * will be nulled.
 285  
      * 3. the parent of the new child is set to point to this node.
 286  
      * 4. if this node already had a left child, the parent of the old
 287  
      * child is nulled.
 288  
      * If an exception happens, all changes will be undone.
 289  
      *
 290  
      * @param node  the new child.
 291  
      *
 292  
      * @throws DataRangeMismatchException  if the data range of the new
 293  
      * child is not adjacent to the datarange of the other child.
 294  
      * @throws RuntimeException  if the node is already the right child
 295  
      * of this node.
 296  
      * @throws RuntimeException  if this node is already finalized.
 297  
      */
 298  
     public void setRightChild(HCTreeNode node) throws DataRangeMismatchException {
 299  
 
 300  
         HCTreeNode oldChild;
 301  
 
 302  84
         assertFinalized();
 303  
 
 304  
         // if this is already the right child, we are done.
 305  
         // We need to return here, to avoid problems later on.
 306  83
         if (this.rightChild == node) return;
 307  
 
 308  
         // if this is already the left child, this is illegal.
 309  82
         if ((node != null) && (this.leftChild == node))
 310  1
             throw new RuntimeException();
 311  
 
 312  
 
 313  81
         oldChild = this.rightChild;
 314  81
         this.rightChild = node;
 315  
 
 316  
         try {
 317  81
             if (node != null) node.setParent(this);
 318  0
         } catch (NotAChildException e) {
 319  
             ; // this never happens.
 320  81
         }
 321  
 
 322  
         try {
 323  81
             if (oldChild != null) oldChild.setParent(null);
 324  0
         } catch (NotAChildException e) {
 325  
             ; // this never happens.
 326  81
         }
 327  
 
 328  81
         this.updateDataRange();
 329  81
     }
 330  
 
 331  
     /**
 332  
      * Sets the parent of this node.
 333  
      * This method is not usable on its own. The only legal way to call
 334  
      * it is from the setLeftChild() and setRightChild() methods.
 335  
      * I.e. this method can be called only after the child of the parent
 336  
      * is already set to point to this node.
 337  
      *
 338  
      * The method can indirectly affect two nodes (in this order.)
 339  
      * 1. the reference to this node from the old parent is removed.
 340  
      * 2. The parent reference of this node is set as asked.
 341  
      *
 342  
      * If an exception happens, all changes will be undone.
 343  
      *
 344  
      * @param node  the new parent.
 345  
      *
 346  
      * @throws NotAChildException  if this node is not a child of the
 347  
      * specified parent.
 348  
      * @throws RuntimeException  if this node is already finalized.
 349  
      */
 350  
     public void setParent(HCTreeNode parent) throws NotAChildException {
 351  
 
 352  166
         assertFinalized();
 353  
 
 354  166
         if (
 355  
             (parent != null) &&
 356  
             (parent.getRightChild() != this) &&
 357  
             (parent.getLeftChild() != this)
 358  
         )
 359  1
             throw new NotAChildException(
 360  
                     "parent given to setParent() doesn't have this child.");
 361  
 
 362  
         // clean up old parent.
 363  165
         if (this.parent != null) {
 364  
             try {
 365  6
                 if (this.parent.getLeftChild() == this)
 366  1
                     this.parent.setLeftChild(null);
 367  5
                 else if (this.parent.getRightChild() == this)
 368  1
                     this.parent.setRightChild(null);
 369  0
             } catch (DataRangeMismatchException e) {
 370  
 
 371  
                 // this never happens. The data ranges cannot mismatch
 372  
                 // as there will be maximum one child.
 373  
                 ;
 374  
 
 375  
 
 376  6
             }
 377  
         }
 378  
 
 379  165
         this.parent = parent;
 380  165
     }
 381  
 
 382  
     /**
 383  
      * Finalizes the tree. After executing this
 384  
      *   - every node either has either no or exactly two children.
 385  
      *   - height of a node is higher than the heights of its children.
 386  
      *   - height of leaf nodes is zero.
 387  
      *   - every leaf node has width 1 datarange.
 388  
      *   - the data ranges of leaf nodes are in correct order, i.e.
 389  
      *     the first child counting from left is 0, then 1, etc.
 390  
      * This method really does nothing, but throws exceptions if necessary.
 391  
      *
 392  
      * @throws IllegalArgumentException  if this node is not a root node of
 393  
      * a tree.
 394  
      * @throws IllegalArgumentException  if any node in this root
 395  
      * doesn't meet all the criteria mentioned above.
 396  
      * a tree.
 397  
      */
 398  
     public void finalizeTree() throws IllegalArgumentException {
 399  
 
 400  35
         if (this.parent != null) throw new IllegalArgumentException(
 401  
                 "You may only finalize full trees.");
 402  
 
 403  35
         this.finalizeTreeRecursively(0);
 404  31
     }
 405  
 
 406  
     /**
 407  
      * A helper for finalizeTree()
 408  
      *
 409  
      * @param index  the index expected for the next child.
 410  
      *
 411  
      * @throws IllegalArgumentException  if this node does not
 412  
      * meet the criteria described in the documentation of
 413  
      * {@link finalizeTree}.
 414  
      *
 415  
      * @return  the index expected for the next child.
 416  
      */
 417  
     private int finalizeTreeRecursively(int index) {
 418  
 
 419  167
         if ((this.leftChild == null) && (this.rightChild == null)) {
 420  
 
 421  
             // A leaf node
 422  98
             if (this.height != 0) throw new IllegalArgumentException(
 423  
                 "Height of a clustering tree leaf node is " + this.height
 424  
             );
 425  98
             if (!this.dataRange.equals(new DataRange(index,index)))
 426  1
                 throw new IllegalArgumentException(
 427  
                     "Expecting index "+index+", but got range "+this.dataRange);
 428  97
             return index + 1;
 429  
 
 430  69
         } else if ((this.leftChild != null) && (this.rightChild != null)) {
 431  
 
 432  67
             if (
 433  
                 (this.height < this.leftChild.getHeight()) ||
 434  
                 (this.height < this.rightChild.getHeight())
 435  1
             ) throw new IllegalArgumentException(
 436  
                 "A height of a node is lower than the height of its child.");
 437  
 
 438  66
             index = this.leftChild.finalizeTreeRecursively(index);
 439  66
             index = this.rightChild.finalizeTreeRecursively(index);
 440  
 
 441  66
             this.finalized = true;
 442  
 
 443  66
             return index;
 444  
 
 445  2
         } else throw new IllegalArgumentException(
 446  
             "A clustering tree node must have either zero or two children."
 447  
         );
 448  
 
 449  
     }
 450  
 
 451  
 }