package rtree;
import java.util.*;
import rtree.RTree.Processor;
/**
* 3D R-Tree implementation for minecraft.
* Uses algorithms from: http://www.sai.msu.su/~megera/postgres/gist/papers/Rstar3.pdf
* @author Colonel32
*/
public class RTree
{
private Node root;
private int maxSize;
private int minSize;
private NodeSplitter splitter;
public static interface Processor
{
/**
* @return true if processing should continue, false otherwise
*/
public boolean process(BoundedObject bo);
}
private class Node implements BoundedObject
{
Node parent;
AABB box;
ArrayList<Node> children;
ArrayList<BoundedObject> data;
public Node()
{
}
public Node(boolean isLeaf)
{
if(isLeaf)
data = new ArrayList<BoundedObject>(maxSize+1);
else
children = new ArrayList<Node>(maxSize+1);
}
public boolean isLeaf() { return data != null; }
public boolean isRoot() { return parent == null; }
public void addTo(Node parent)
{
assert(parent.children != null);
parent.children.add(this);
this.parent = parent;
computeMBR();
splitter.split(parent);
}
public void computeMBR()
{
computeMBR(true);
}
public void computeMBR(boolean doParents)
{
if(box == null) box = new AABB();
if(!isLeaf())
{
if(children.isEmpty()) return;
children.get(0).box.cloneInto(box);
for(int i=1; i<children.size(); i++)
box.merge(children.get(i).box);
}
else
{
if(data.isEmpty()) return;
data.get(0).getBounds().cloneInto(box);
for(int i=1; i<data.size(); i++)
box.merge(data.get(i).getBounds());
}
if(doParents && parent != null) parent.computeMBR();
}
public void remove()
{
if(parent == null)
{
assert(root == this);
root = null;
return;
}
parent.children.remove(this);
if(parent.children.isEmpty())
parent.remove();
else
parent.computeMBR();
}
public ArrayList<? extends BoundedObject> getSubItems() { return isLeaf() ? data : children; }
public AABB getBounds() { return box; }
public boolean contains(int px, int py, int pz) { return box.contains(px,py,pz); }
public int size() { return isLeaf() ? data.size() : children.size(); }
public int depth()
{
Node n = this;
int d = 0;
while(n != null)
{
n = n.parent;
d++;
}
return d;
}
public String toString()
{
return "Depth: "+depth()+", size: "+size();
}
}
/**
* Node splitting algorithms selectors.
*/
public enum SplitterType
{
/**
* Quadratic splitting algorithm. Runs in O(n^2) time where n = maxChildren+1.
* Use this for R-trees that update with new data fairly often.
*/
QUADRATIC,
/**
* NOT IMPLEMENTED.
* Exhaustive splitting algorithm. Runs in O(2^n) time where n = maxChildren+1.
* Produces a more optimized result than the quadratic algorithm
* at the cost of a high runtime. Use this for R-trees that update rarely
* and queried frequently, such as zone protection.
*/
EXHAUSTIVE,
}
private interface NodeSplitter
{
void split(Node n);
}
private class QuadraticNodeSplitter implements NodeSplitter
{
public void split(Node n)
{
if(n.size() <= maxSize) return;
boolean isleaf = n.isLeaf();
// Choose seeds. Would write a function for this, but it requires returning 2 objects
BoundedObject seed1 = null, seed2 = null;
ArrayList<? extends BoundedObject> list;
if(isleaf)
list = n.data;
else
list = n.children;
int maxD = Integer.MIN_VALUE;
AABB box = new AABB();
for(int i=0; i<list.size(); i++)
for(int j=0; j<list.size(); j++)
{
if(i == j) continue;
BoundedObject n1 = list.get(i), n2 = list.get(j);
n1.getBounds().cloneInto(box);
box.merge(n2.getBounds());
int d = box.getVolume() - n1.getBounds().getVolume() - n2.getBounds().getVolume();
if(d > maxD)
{
maxD = d;
seed1 = n1;
seed2 = n2;
}
}
assert(seed1 != null && seed2 != null);
// Distribute
Node group1 = new Node(isleaf);
group1.box = seed1.getBounds().clone();
Node group2 = new Node(isleaf);
group2.box = seed2.getBounds().clone();
if(isleaf)
distributeLeaves(n, group1, group2);
else
distributeBranches(n, group1, group2);
Node parent = n.parent;
if(parent == null)
{
parent = new Node(false);
root = parent;
}
else
parent.children.remove(n);
group1.parent = parent;
parent.children.add(group1);
group1.computeMBR();
split(parent);
group2.parent = parent;
parent.children.add(group2);
group2.computeMBR();
split(parent);
}
private void distributeBranches(Node n, Node g1, Node g2)
{
assert(!(n.isLeaf() || g1.isLeaf() || g2.isLeaf()));
while(!n.children.isEmpty() && g1.children.size() < maxSize - minSize + 1 &&
g2.children.size() < maxSize - minSize + 1)
{
// Pick next
int difmax = Integer.MIN_VALUE;
int nmax_index = -1;
for(int i=0; i<n.children.size(); i++)
{
Node node = n.children.get(i);
int dif = Math.abs(node.box.expansionNeeded(g1.box) - node.box.expansionNeeded(g2.box));
if(dif > difmax)
{
difmax = dif;
nmax_index = i;
}
}
assert(nmax_index != -1);
// Distribute Entry
Node nmax = n.children.remove(nmax_index);
Node parent = null;
// ... to the one with the least expansion
int overlap1 = nmax.box.expansionNeeded(g1.box);
int overlap2 = nmax.box.expansionNeeded(g2.box);
if(overlap1 > overlap2) parent = g1;
else if(overlap2 > overlap1) parent = g2;
else
{
// Or the one with the lowest volume
int vol1 = g1.box.getVolume();
int vol2 = g2.box.getVolume();
if(vol1 > vol2) parent = g2;
else if(vol2 > vol1) parent = g1;
else
{
// Or the one with the least items
if(g1.children.size() < g2.children.size()) parent = g1;
else parent = g2;
}
}
assert(parent != null);
parent.children.add(nmax);
nmax.parent = parent;
}
if(!n.children.isEmpty())
{
Node parent = null;
if(g1.children.size() == maxSize - minSize + 1)
parent = g2;
else
parent = g1;
for(int i=0; i<n.children.size(); i++)
{
parent.children.add(n.children.get(i));
n.children.get(i).parent = parent;
}
n.children.clear();
}
}
private void distributeLeaves(Node n, Node g1, Node g2)
{
// Same process as above; just different types.
assert(n.isLeaf() && g1.isLeaf() && g2.isLeaf());
while(!n.data.isEmpty() && g1.data.size() < maxSize - minSize + 1 &&
g2.data.size() < maxSize - minSize + 1)
{
// Pick next
int difmax = Integer.MIN_VALUE;
int nmax_index = -1;
for(int i=0; i<n.data.size(); i++)
{
BoundedObject node = n.data.get(i);
int d1 = node.getBounds().expansionNeeded(g1.box);
int d2 = node.getBounds().expansionNeeded(g2.box);
int dif = Math.abs(d1 - d2);
if(dif > difmax)
{
difmax = dif;
nmax_index = i;
}
}
assert(nmax_index != -1);
// Distribute Entry
BoundedObject nmax = n.data.remove(nmax_index);
// ... to the one with the least expansion
int overlap1 = nmax.getBounds().expansionNeeded(g1.box);
int overlap2 = nmax.getBounds().expansionNeeded(g2.box);
if(overlap1 > overlap2) g1.data.add(nmax);
else if(overlap2 > overlap1) g2.data.add(nmax);
else
{
int vol1 = g1.box.getVolume();
int vol2 = g2.box.getVolume();
if(vol1 > vol2) g2.data.add(nmax);
else if(vol2 > vol1) g1.data.add(nmax);
else
{
if(g1.data.size() < g2.data.size()) g1.data.add(nmax);
else g2.data.add(nmax);
}
}
}
if(!n.data.isEmpty())
{
if(g1.data.size() == maxSize - minSize + 1)
g2.data.addAll(n.data);
else
g1.data.addAll(n.data);
n.data.clear();
}
}
}
/**
* Creates an R-Tree. Sets the splitting algorithm to quadratic splitting.
* @param minChildren Minimum children in a node. {@code 2 <= minChildren <= maxChildren/2}
* @param maxChildren Maximum children in a node. Node splits at this number + 1
*/
public RTree(int minChildren, int maxChildren)
{
this(minChildren, maxChildren, SplitterType.QUADRATIC);
}
public RTree(int minChildren, int maxChildren, SplitterType splittertyp)
{
if(minChildren < 2 || minChildren > maxChildren/2) throw new IllegalArgumentException("2 <= minChildren <= maxChildren/2");
switch(splittertyp)
{
case QUADRATIC:
splitter = new QuadraticNodeSplitter();
break;
case EXHAUSTIVE:
throw new UnsupportedOperationException("Not implemented yet.");
default:
throw new RuntimeException("Invalid node splitter");
}
this.minSize = minChildren;
this.maxSize = maxChildren;
root = null;
}
/**
* Adds items whose AABB intersects the query AABB to results
* @param results A collection to store the query results
* @param box The query... if null all items are returned
*/
public void query(Collection<? super BoundedObject> results, AABB box)
{
query(results, box, root);
}
private void query(Collection<? super BoundedObject> results, AABB box, Node node)
{
if(node == null) return;
if(node.isLeaf())
{
for(int i=0; i<node.data.size(); i++)
if(box == null || node.data.get(i).getBounds().overlaps(box))
results.add(node.data.get(i));
}
else
{
for(int i=0; i<node.children.size(); i++)
if(box == null || node.children.get(i).box.overlaps(box))
query(results, box, node.children.get(i));
}
}
public boolean query(Processor processor, AABB box)
{
return query(processor, box, root);
}
private boolean query(Processor processor, AABB box, Node node)
{
if(node == null) return true;
if(node.isLeaf())
{
for(int i=0; i<node.data.size(); i++)
if(box == null || node.data.get(i).getBounds().overlaps(box))
if(!processor.process(node.data.get(i)))
return false;
}
else
{
for(int i=0; i<node.children.size(); i++)
if(box == null || node.children.get(i).box.overlaps(box))
if(!query(processor, box, node.children.get(i)))
return false;
}
return true;
}
/**
* Returns one item that intersects the query box, or null if nothing intersects
* the query box.
*/
public BoundedObject queryOne(AABB box)
{
return queryOne(box,root);
}
private BoundedObject queryOne(AABB box, Node node)
{
if(node == null) return null;
if(node.isLeaf())
{
for(int i=0; i<node.data.size(); i++)
if(node.data.get(i).getBounds().overlaps(box))
return node.data.get(i);
return null;
}
else
{
for(int i=0; i<node.children.size(); i++)
if(node.children.get(i).box.overlaps(box))
{
BoundedObject result = queryOne(box,node.children.get(i));
if(result != null) return result;
}
return null;
}
}
/**
* Adds items whose AABB contains the specified point.
* @param results A collection to store the query results.
* @param px Point X coordinate
* @param py Point Y coordinate
* @param pz Point Z coordinate
*/
public void query(Collection<? super BoundedObject> results, int px, int py, int pz)
{
query(results, px, py, pz, root);
}
private void query(Collection<? super BoundedObject> results, int px, int py, int pz, Node node)
{
if(node == null) return;
if(node.isLeaf())
{
for(int i=0; i<node.data.size(); i++)
if(node.data.get(i).getBounds().contains(px,py,pz))
results.add(node.data.get(i));
}
else
{
for(int i=0; i<node.children.size(); i++)
if(node.children.get(i).box.contains(px,py,pz))
query(results, px,py,pz, node.children.get(i));
}
}
/**
* Returns one item that intersects the query point, or null if no items intersect that point.
*/
public BoundedObject queryOne(int px, int py, int pz)
{
return queryOne(px,py,pz,root);
}
private BoundedObject queryOne(int px, int py, int pz, Node node)
{
if(node == null) return null;
if(node.isLeaf())
{
for(int i=0; i<node.data.size(); i++)
if(node.data.get(i).getBounds().contains(px,py,pz))
return node.data.get(i);
return null;
}
else
{
for(int i=0; i<node.children.size(); i++)
if(node.children.get(i).box.contains(px,py,pz))
{
BoundedObject result = queryOne(px,py,pz, node.children.get(i));
if(result != null) return result;
}
return null;
}
}
/**
* Removes the specified object if it is in the tree.
* @param o
*/
public void remove(BoundedObject o)
{
Node n = chooseLeaf(o, root);
assert(n.isLeaf());
if(!n.data.remove(o))
assert(false);
//T.E. commented out, there was no weird bug, it was my fault
// Although this should help the memory leak, it's not too serious
// //T.E. I added this call to remove if the data is empty due to some weird bug
// //when removing and adding. I surmised that, without if you remove the last data point
// // the leaf stays there with the same area, which is a memory leak as well.
// if(n.data.isEmpty())
// n.remove();
// else
n.computeMBR();
}
/**
* Inserts object o into the tree. Note that if the value of o.getAABB() changes
* while in the R-tree, the result is undefined.
* @throws NullPointerException If o == null
*/
public void insert(BoundedObject o)
{
if(o == null) throw new NullPointerException("Cannot store null object");
if(root == null)
root = new Node(true);
Node n = chooseLeaf(o, root);
assert(n.isLeaf());
n.data.add(o);
n.computeMBR();
splitter.split(n);
}
/**
* Counts the number of items in the tree.
*/
public int count()
{
if(root == null) return 0;
return count(root);
}
private int count(Node n)
{
assert(n != null);
if(n.isLeaf())
{
return n.data.size();
}
else
{
int sum = 0;
for(int i=0; i<n.children.size(); i++)
sum += count(n.children.get(i));
return sum;
}
}
private Node chooseLeaf(BoundedObject o, Node n)
{
assert(n != null);
if(n.isLeaf()) return n;
else
{
AABB box = o.getBounds();
int maxOverlap = Integer.MAX_VALUE;
Node maxnode = null;
for(int i=0; i<n.children.size(); i++)
{
int overlap = n.children.get(i).box.expansionNeeded(box);
if((overlap < maxOverlap)
|| (overlap == maxOverlap
&& n.children.get(i).box.getVolume() < maxnode.box.getVolume()))
{
maxOverlap = overlap;
maxnode = n.children.get(i);
}
}
if(maxnode == null) // Not sure how this could occur
return null;
return chooseLeaf(o,maxnode);
}
}
}