/* * Copyright 2017 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.utilities.tree; import com.google.common.collect.Ordering; import com.google.common.collect.TreeMultimap; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.NavigableSet; import java.util.Set; /** * A data structure that allows to add, remove and find nearest nodes in an N-dimensional space. * This can be used to locate entities (or block entities) nearest to player of a specific type, provided that system * using it keeps track of all loaded & active entities of this specific type. * <br><br> * For three dimensions this is an octree and for two dimensions this is quadtree implementations. * <br><br> * It is possible to provide a DistanceFunction in the constructor that will be used instead of EuclideanDistanceFunction. * This allows to prefer a specific axis (i.e. objects above/below have higher priority than front/back or sides) or * it might have some other applications. Please note, that if it is used, the distance returned in an "Entry" is * according to the DistanceFunction. * * @param <T> The type of object stored as a value in this SpaceTree. */ public class SpaceTree<T> extends AbstractDimensionalMap<T> { private static final int DEFAULT_BUCKET_SIZE = 24; private static final DistanceFunction DEFAULT_DISTANCE_FUNCTION = new EuclideanDistanceFunction(); private final int bucketSize; private final int dimensions; private final int subNodeCount; private final DistanceFunction distanceFunction; private Node rootNode; /** * Constructor for a SpaceTree object given only the dimensions. The object will use the DEFAULT_BUCKET_SIZE * and DEFAULT_DISTANCE_FUNCTION as the default bucket size and distance function respectively. */ public SpaceTree(int dimensions) { this(dimensions, DEFAULT_BUCKET_SIZE, DEFAULT_DISTANCE_FUNCTION); } /** * Constructor for a SpaceTree object given only the dimensions and the default distance function. * The object will use the DEFAULT_BUCKET_SIZE for the default bucket size. */ public SpaceTree(int dimensions, DistanceFunction distanceFunction) { this(dimensions, DEFAULT_BUCKET_SIZE, distanceFunction); } /** * Constructor for a SpaceTree object given only the dimensions and the default bucket size. * The object will use the DEFAULT_DISTANCE_FUNCTION for the default distance function. */ public SpaceTree(int dimensions, int bucketSize) { this(dimensions, bucketSize, DEFAULT_DISTANCE_FUNCTION); } /** * Constructor for a SpaceTree object given only the dimensions, default bucket size, and * default distance function. */ public SpaceTree(int dimensions, int bucketSize, DistanceFunction distanceFunction) { this.dimensions = dimensions; this.bucketSize = bucketSize; this.distanceFunction = distanceFunction; int subNodes = 2; for (int i = 1; i < dimensions; i++) { subNodes *= 2; } subNodeCount = subNodes; } /** * Adds a new value to the SpaceTree. If the root (start of the tree) is null, then a new node will be * created. Otherwise, it'll call upon the addToNode() method to add the new value to the root node. * * @param position The position of the new node */ @Override public T add(float[] position, T value) { validatePosition(position); if (value == null) { throw new IllegalArgumentException("Value cannot be null"); } if (rootNode == null) { // Create a new node and make it root float[] min = new float[dimensions]; float[] max = new float[dimensions]; for (int i = 0; i < dimensions; i++) { min[i] = Float.MIN_VALUE; max[i] = Float.MAX_VALUE; } rootNode = createNewNode(position, min, max, value); return null; } else { // Add a node to the root return addToNode(position, rootNode, value); } } /** * Removes a node and then returns the value of the removed node. * * @param position The position of the node to be removed * @return a T object which is the value of the node removed (null if root node was null) */ @Override public T remove(float[] position) { validatePosition(position); if (rootNode == null) { return null; } else { // If the node to be removed is not a leaf, then reattach the tree so the removed node's children remain in the tree if (rootNode.center != null) { // This is not a leaf int subNodeIndex = getSubNodeIndex(position, rootNode.center); if (subNodeIndex == -1) { Node oldRootNode = rootNode; rootNode = null; for (Node subNode : oldRootNode.subNodes) { addAllFromNode(subNode, null); } return oldRootNode.centerValue; } else { return removeFromSubNodeOfNode(position, rootNode, subNodeIndex); } } else { // If the node to be removed is a leaf, just remove it // This is a leaf so need to check bucket for (NodeEntry<T> nodeEntry : rootNode.nodeBucket) { if (distanceFunction.getDistance(nodeEntry.position, position) == 0) { rootNode.nodeBucket.remove(nodeEntry); if (rootNode.nodeBucket.size() == 0) { rootNode = null; } return nodeEntry.value; } } // It was not found in this leaf's bucket return null; } } } /** * Returns a collection of entry objects that was created as a result of the search. The search is finding 'count' number of * nodes which are within 'within' distance away from the node specified by the position * * @param position The position of the node to be searched around * @param count The number of nodes to be found during the search */ @Override public Collection<Entry<T>> findNearest(float[] position, int count, float within) { validatePosition(position); if (count < 1) { throw new IllegalArgumentException("Count cannot be smaller than 1"); } if (within < 0) { throw new IllegalArgumentException("Within cannot be smaller than 0"); } if (rootNode == null) { return Collections.emptyList(); } else { TreeSearch<T> treeSearch = new TreeSearch<>(within, count); executeSearchInNode(position, rootNode, treeSearch); return Collections.unmodifiableCollection(treeSearch.results.values()); } } /** * The method for executing the search. * * @param position The position of the node * @param node The node to be searched around * @param treeSearch The treeSearch object for searching */ private void executeSearchInNode(float[] position, Node node, TreeSearch<T> treeSearch) { if (node.center != null) { // This is not a leaf float distance = distanceFunction.getDistance(position, node.center); if (distance <= treeSearch.maxDistance) { treeSearch.addEntry(new Entry<>(distance, node.centerValue)); } for (Node subNode : node.subNodes) { if (subNode != null) { if (distanceFunction.getPointRegionDistance(position, subNode.minValues, subNode.maxValues) <= treeSearch.maxDistance) { executeSearchInNode(position, subNode, treeSearch); } } } } else { // This is a leaf so need to check bucket for (NodeEntry<T> nodeEntry : node.nodeBucket) { float distance = distanceFunction.getDistance(nodeEntry.position, position); if (distance <= treeSearch.maxDistance) { treeSearch.addEntry(new Entry<>(distance, nodeEntry.value)); } } } } /** * Removes and returns the value of the subNode removed from the node. * * @param position The position of the node * @param node The parent node * @param subNodeIndex The index of the subNode to be removed * @return the value of the subNode removed */ private T removeFromSubNodeOfNode(float[] position, Node node, int subNodeIndex) { Node processedNode = node; int processedSubNodeIndex = subNodeIndex; while (true) { Node subNode = processedNode.subNodes[processedSubNodeIndex]; if (subNode == null) { return null; } else { if (subNode.center != null) { // This is not a leaf int subSubNodeIndex = getSubNodeIndex(position, subNode.center); if (subSubNodeIndex == -1) { processedNode.subNodes[processedSubNodeIndex] = null; for (Node subSubNode : subNode.subNodes) { addAllFromNode(subSubNode, processedNode); } return subNode.centerValue; } else { processedNode = subNode; processedSubNodeIndex = subSubNodeIndex; } } else { // It is a leaf so need to check bucket for (NodeEntry<T> nodeEntry : subNode.nodeBucket) { if (distanceFunction.getDistance(nodeEntry.position, position) == 0) { subNode.nodeBucket.remove(nodeEntry); if (subNode.nodeBucket.size() == 0) { processedNode.subNodes[processedSubNodeIndex] = null; } return nodeEntry.value; } } } } } } /** * If nodeToAddFrom is not null, then the nodeToAddFrom will be added to the nodeToAddTo. * * @param nodeToAddFrom The node to be added to the nodeToAddTo * @param nodeToAddTo The node to receive the nodeToAddFrom */ private void addAllFromNode(Node nodeToAddFrom, Node nodeToAddTo) { if (nodeToAddFrom != null) { if (nodeToAddTo == null) { add(nodeToAddFrom.center, nodeToAddFrom.centerValue); } else { addToNode(nodeToAddFrom.center, nodeToAddTo, nodeToAddFrom.centerValue); } for (Node subNode : nodeToAddFrom.subNodes) { addAllFromNode(subNode, nodeToAddTo); } } } /** * A method used to add a node with a value to another existing node. * * @param position The position of the new node * @param node The node to add to * @param value The value of the new node * @return the value of the node that was added */ private T addToNode(float[] position, Node node, T value) { Node processedNode = node; while (true) { if (processedNode.center != null) { // This is not a leaf int subNodeIndex = getSubNodeIndex(position, processedNode.center); if (subNodeIndex == -1) { T oldValue = processedNode.centerValue; processedNode.centerValue = value; return oldValue; } else { Node subNode = processedNode.subNodes[subNodeIndex]; if (subNode == null) { float[] min = new float[dimensions]; float[] max = new float[dimensions]; for (int i = 0; i < dimensions; i++) { if (position[i] > processedNode.center[i]) { min[i] = processedNode.center[i]; max[i] = processedNode.maxValues[i]; } else { min[i] = processedNode.minValues[i]; max[i] = processedNode.center[i]; } } processedNode.subNodes[subNodeIndex] = createNewNode(position, min, max, value); return null; } else { processedNode = subNode; } } } else { // This is a leaf, so need to check bucket // First check if the bucket already contains value for this position for (NodeEntry<T> nodeEntry : processedNode.nodeBucket) { if (distanceFunction.getDistance(nodeEntry.position, position) == 0) { processedNode.nodeBucket.remove(nodeEntry); processedNode.nodeBucket.add(new NodeEntry<>(position, value)); return nodeEntry.value; } } processedNode.nodeBucket.add(new NodeEntry<>(position, value)); if (processedNode.nodeBucket.size() > bucketSize) { processedNode.splitNode(); } return null; } } } /** * Obtains the index of the subNode specified by center of the node specified by position. * * @param position The position of the parent node * @param center The position of the subNode * @return the index of the sub Node of the node specified by the position */ private int getSubNodeIndex(float[] position, float[] center) { int index = 0; int increment = 1; for (int i = 0; i < dimensions; i++) { if (position[i] > center[i]) { index += increment; } increment *= 2; } if (index == 0) { if (distanceFunction.getDistance(position, center) == 0) { return -1; } } return index; } /** * Creates a new node. * * @param position The position of the new node * @param min The minimal position of the new node * @param max The maximal position of the new node * @param value The value of the new node */ private Node createNewNode(float[] position, float[] min, float[] max, T value) { float[] positionCopy = new float[dimensions]; System.arraycopy(position, 0, positionCopy, 0, dimensions); return new Node(positionCopy, value, min, max); } /** * Throws a new IllegalArguementException if the position is either null or the length of the * position (number of items in the array) is not equal to dimensions */ private void validatePosition(float[] position) { if (position == null || position.length != dimensions) { throw new IllegalArgumentException("Invalid position, either null or invalid number of dimensions"); } } /** * The class that is responsible for searching the SpaceTree */ private static final class TreeSearch<T> { private float maxDistance; private int maxCapacity; private TreeMultimap<Float, Entry<T>> results = TreeMultimap.create( new Comparator<Float>() { @Override public int compare(Float o1, Float o2) { return o1.compareTo(o2); } }, Ordering.arbitrary()); /** * Constructor for the TreeSearch object. * * @param maxDistance The max distance of the TreeSearch object will search * @param maxCapacity The maximum capacity of the TreeSearch object for nodes */ private TreeSearch(float maxDistance, int maxCapacity) { this.maxDistance = maxDistance; this.maxCapacity = maxCapacity; } /** * Adds an object to the results of the search as long as maxCapacity nor maxDistance is not exceeded. * * @param entry The object to add to the results of the search */ void addEntry(Entry<T> entry) { results.put(entry.distance, entry); int size = results.size(); if (size > maxCapacity) { //Removes some entries if maxCapacity is exceeded Float maxDistanceInResults = results.keySet().last(); NavigableSet<Entry<T>> entriesAtThisDistance = results.get(maxDistanceInResults); entriesAtThisDistance.pollLast(); if (entriesAtThisDistance.size() == 0) { results.removeAll(maxDistanceInResults); } maxDistance = results.keySet().last(); } else if (size == maxCapacity) { //If the size is at maxCapacity, then set maxDistance to the distance of the last entry maxDistance = results.keySet().last(); } } } /** * The supporting data structure for the Node object. It holds the position, as well as the value of a node. */ private static final class NodeEntry<T> { private float[] position; private T value; /** * Constructor for the NodeEntry object. * * @param position The position information that the NodeEntry will hold * @param value The value which the NodeEntry will hold */ private NodeEntry(float[] position, T value) { this.position = position; this.value = value; } } /** * The supporting data structure for the SpaceTree object. The SpaceTree made up of Node objects. */ private final class Node { private Set<NodeEntry<T>> nodeBucket; private float[] center; private T centerValue; private float[] minValues; private float[] maxValues; private Node[] subNodes; /** * The Constructor for the Node Object. * * @param position The position of the node * @param value The value which the node holds * @param minValues The minimum values of the node in terms of position * @param maxValues The maximum values of the node in terms of position */ private Node(float[] position, T value, float[] minValues, float[] maxValues) { nodeBucket = new HashSet<>(); nodeBucket.add(new NodeEntry<>(position, value)); this.minValues = minValues; this.maxValues = maxValues; } /** * Splits the node into two nodes to help maintain order within the tree. */ void splitNode() { Iterator<NodeEntry<T>> iterator = nodeBucket.iterator(); NodeEntry<T> newCenter = iterator.next(); center = newCenter.position; centerValue = newCenter.value; subNodes = new SpaceTree.Node[subNodeCount]; while (iterator.hasNext()) { NodeEntry<T> nodeEntry = iterator.next(); int subNodeIndex = getSubNodeIndex(nodeEntry.position, center); if (subNodes[subNodeIndex] == null) { float[] min = new float[dimensions]; float[] max = new float[dimensions]; for (int i = 0; i < dimensions; i++) { if (nodeEntry.position[i] > center[i]) { min[i] = center[i]; max[i] = maxValues[i]; } else { min[i] = minValues[i]; max[i] = center[i]; } } subNodes[subNodeIndex] = new Node(nodeEntry.position, nodeEntry.value, min, max); } else { subNodes[subNodeIndex].nodeBucket.add(nodeEntry); } } nodeBucket = null; } } }