Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
HCTreeNode |
|
| 4.25;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 | } |