/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.nui.widgets.treeView; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import org.terasology.module.sandbox.API; import java.util.ArrayDeque; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; /** * A general-purpose tree data structure. * * @param <T> Type of objects stored in the tree. */ @API public abstract class Tree<T> { private static final String NULL_NODE_ARGUMENT = "node argument is null"; private static final String NODE_ARGUMENT_INVALID_PARENT = "node argument is not a child of this tree"; /** * The parent of this tree. */ protected Tree<T> parent; /** * The children of this tree. */ protected List<Tree<T>> children = Lists.newArrayList(); /** * The object stored in this tree. */ protected T value; /** * Whether the tree is expanded, i.e. its' child elements are visible. */ private boolean expanded; /** * @return The object stored in this tree. */ public T getValue() { return this.value; } /** * @param value The new value of the object stored in this tree. */ public void setValue(T value) { this.value = value; } /** * @return Whether this tree is expanded. */ public boolean isExpanded() { return this.expanded; } /** * @param expanded The new expanded state of this tree. */ public void setExpanded(boolean expanded) { this.expanded = expanded; } /** * @return Whether the tree is a root (has no parent node). */ public boolean isRoot() { return this.parent == null; } /** * @return Whether the tree is a leaf (has no child nodes). */ public boolean isLeaf() { return this.getChildren().isEmpty(); } /** * @return This tree's parent. */ public Tree<T> getParent() { return this.parent; } /** * @param tree The tree that the parent of this tree is to be set to. */ public void setParent(Tree<T> tree) { this.parent = tree; } /** * @return The list of children for this tree. */ public Collection<Tree<T>> getChildren() { return this.children; } /** * @param tree The tree the index of which is to be returned. * @return The index of the specified tree. */ public int getIndex(Tree<T> tree) { Preconditions.checkNotNull(tree, NULL_NODE_ARGUMENT); return this.children.indexOf(tree); } /** * @return The root of the tree this subtree is a member of. */ public Tree<T> getRoot() { if (this.isRoot()) { return this; } return this.getParent().getRoot(); } /** * @return The depth of the tree this tree is a subtree of. */ public int getDepth() { return this.getRecursiveDepth(0); } private int getRecursiveDepth(int currentDepth) { if (this.isRoot()) { return currentDepth; } return this.getParent().getRecursiveDepth(currentDepth + 1); } /** * @param child A specified tree. * @return Whether the specified tree is a (direct) child of this tree. */ public boolean containsChild(Tree<T> child) { return this.children.contains(child); } /** * @param child The child to be added. * @return Whether the specified child can be added to the tree. */ public boolean acceptsChild(Tree<T> child) { // Only non-null children are allowed. if (child == null) { return false; } // Can't make an item a child of itself. if (this == child) { return false; } if (this.isChildOf(child)) { return false; } return true; } private boolean isChildOf(Tree<T> node) { if (this.parent == node) { return true; } if (this.isRoot()) { return false; } return this.parent.isChildOf(node); } public int indexOf(Tree<T> child) { return this.children.indexOf(child); } /** * Instantiates and adds a child with a specified value to this tree. * A child should not be added if the tree does not accept it. * * @param childValue The value of the child to be added. */ public abstract void addChild(T childValue); /** * Adds a child to this tree. * A child should not be added if the tree does not accept it. * * @param child The child to be added. */ public void addChild(Tree<T> child) { if (this.acceptsChild(child)) { this.children.add(child); child.setParent(this); } } /** * Adds a child to this tree at a specified index. * A child should not be added if the tree does not accept it. * * @param index The index of the child to be added. * @param child The child to be added. */ public void addChild(int index, Tree<T> child) { if (this.acceptsChild(child)) { this.children.add(index, child); child.setParent(this); } } /** * Removes a child at the specified index in this tree. * * @param childIndex The index of the child to be removed. */ public void removeChild(int childIndex) { Tree<T> child = this.children.remove(childIndex); child.setParent(null); } /** * Removes a specified child in this tree. * * @param child The child to be removed. */ public void removeChild(Tree<T> child) { Preconditions.checkNotNull(child, NULL_NODE_ARGUMENT); Preconditions.checkState(child.getParent() == this, NODE_ARGUMENT_INVALID_PARENT); this.children.remove(child); child.setParent(null); } /** * @return A shallow copy of this tree. */ public abstract Tree<T> copy(); /** * @param enumerateExpandedOnly Whether the children of non-expanded items are excluded from the enumeration. * @return The iterator of this tree in depth-first, pre-ordered order. */ public Iterator getDepthFirstIterator(boolean enumerateExpandedOnly) { return new DepthFirstIterator(this, enumerateExpandedOnly); } /** * An iterator in depth-first, pre-ordered order. */ private class DepthFirstIterator implements Iterator { private static final String ITERATOR_NO_ITEMS = "no elements left (try validating with hasNext?)"; /** * If true, the children of non-expanded items will be excluded from iteration. */ private boolean enumerateExpandedOnly; private Tree<T> next; private Deque<Enumeration> stack = new ArrayDeque<>(); DepthFirstIterator(Tree<T> root, boolean enumerateExpandedOnly) { this.enumerateExpandedOnly = enumerateExpandedOnly; this.next = root; if (!enumerateExpandedOnly || root.isExpanded()) { this.stack.push(Collections.enumeration(root.getChildren())); } } @Override public boolean hasNext() { return this.next != null; } @Override public Object next() { if (!this.hasNext()) { throw new NoSuchElementException(ITERATOR_NO_ITEMS); } Tree<T> current = next; Enumeration childEnumeration = stack.peek(); // Retrieve the next item. next = traverse(childEnumeration); return current; } private Tree<T> traverse(Enumeration childEnumeration) { // Handle the root object being non-expanded. if (childEnumeration == null) { return null; } if (childEnumeration.hasMoreElements()) { Tree<T> child = (Tree<T>) childEnumeration.nextElement(); // If the child is expanded, iterate through its' children as well. if (!enumerateExpandedOnly || child.isExpanded()) { stack.push(Collections.enumeration(child.getChildren())); } return child; } // If a higher level is available, return to it. stack.pop(); if (stack.isEmpty()) { return null; } else { return traverse(stack.peek()); } } } }