package de.blau.android.util.rtree;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import android.util.Log;
import de.blau.android.exception.OsmException;
import de.blau.android.osm.BoundingBox;
/**
* 2D R-Tree implementation for Android.
* Uses algorithms from: http://www.sai.msu.su/~megera/postgres/gist/papers/Rstar3.pdf
* @author Colonel32
* @author cnvandev
* @author simonpoole
*/
public class RTree implements Serializable {
private static final long serialVersionUID = 1L;
private final static String DEBUG_TAG = RTree.class.getName();
private Node root;
private int maxSize;
private int minSize;
private QuadraticNodeSplitter splitter;
private class Node implements BoundedObject, Serializable {
private static final long serialVersionUID = 1L;
private final String DEBUG_TAG = Node.class.getName();
Node parent;
BoundingBox box;
ArrayList<Node> children;
ArrayList<BoundedObject> data;
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 BoundingBox();
}
if (!isLeaf()) {
if (children.isEmpty()) {
return;
}
box.set(children.get(0).box);
for (int i = 1; i < children.size(); i++) {
box.union(children.get(i).box);
}
} else {
if (data.isEmpty()) {
return;
}
box.set(data.get(0).getBounds());
for (int i = 1; i < data.size(); i++) {
BoundingBox box2 = data.get(i).getBounds();
if (box2.isEmpty()) {
box.union(box2.getLeft(), box2.getTop());
} else {
box.union(box2);
}
}
}
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 BoundingBox getBounds() {
// Log.d(DEBUG_TAG, "box is " + box);
return box;
}
public boolean contains(int px, int py) {
return box.contains(px, py);
}
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();
}
}
private class QuadraticNodeSplitter implements Serializable {
private static final long serialVersionUID = 1L;
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
Object[] list;
if (isleaf) {
list = n.data.toArray();
} else {
list = n.children.toArray();
}
ArrayList<BoundingBox> cachedBox = new ArrayList<BoundingBox>(list.length);
double[] cachedArea = new double[list.length];
for (int i = 0; i < list.length; i++) {
BoundingBox tempBox = ((BoundedObject)list[i]).getBounds();
cachedBox.add(tempBox);
double tempArea = area(tempBox);
cachedArea[i] = tempArea;
}
BoundingBox box = new BoundingBox();
BoundingBox seed1Box = cachedBox.get(0); // Note we need to cater for the degenerate cases
BoundingBox seed2Box = cachedBox.get(list.length-1);
double maxD = -Double.MAX_VALUE;
for (int i = 0; i < list.length; i++) {
for (int j=0; j < list.length; j++) {
// Log.d(DEBUG_TAG," i " + i + " j " + j);
if (i == j) continue;
BoundingBox box1 = cachedBox.get(i), box2 = cachedBox.get(j);
box.set(box1);
double d;
if (box2.isEmpty()) {
if (box.isEmpty()) {
// not sure if this really works
box.union(box2.getLeft(), box2.getTop());
d = area(box);
if (d==0) {
d = box.getRight()-box2.getRight();
if (d==0) {
d = box.getTop()-box2.getTop();
}
// else ... two nodes in the same place
}
} else {
box.union(box2.getLeft(), box2.getTop());
d = area(box) - cachedArea[i];
}
} else {
box.union(box2);
d = area(box) - cachedArea[i] - cachedArea[j];
}
if (d > maxD) {
maxD = d;
seed1Box = box1;
seed2Box = box2;
// Log.d(DEBUG_TAG,"seed1 " + seed1 + " seed2 " + seed2);
}
}
}
// assert(seed1 != null && seed2 != null);
// Log.d(DEBUG_TAG,"seed1 " + seed1 + " seed2 " + seed2);
// Distribute
Node group1 = new Node(isleaf);
group1.box = new BoundingBox(seed1Box);
Node group2 = new Node(isleaf);
group2.box = new BoundingBox(seed2Box);
if (isleaf)
distributeLeaves(n,cachedBox, 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
long difmax = Long.MIN_VALUE;
int nmax_index = -1;
long overlap1 = -1;
long overlap2 = -1;
for (int i = 0; i < n.children.size(); i++) {
Node node = n.children.get(i);
long expansion1 = expansionNeeded(node.box, g1.box);
long expansion2 = expansionNeeded(node.box, g2.box);
long dif = Math.abs(expansion1 - expansion2);
if (dif > difmax) {
difmax = dif;
nmax_index = i;
overlap1 = expansion1;
overlap2 = expansion2;
}
}
// assert(nmax_index != -1);
// Distribute Entry
Node nmax = n.children.remove(nmax_index);
Node parent = null;
// ... to the one with the least expansion
if (overlap1 > overlap2) {
parent = g1;
} else if (overlap2 > overlap1) {
parent = g2;
} else {
// Or the one with the lowest area
double area1 = area(g1.box);
double area2 = area(g2.box);
if (area1 > area2) parent = g2;
else if (area2 > area1) 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, ArrayList<BoundingBox>cache, 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
long difmax = Long.MIN_VALUE;
int nmax_index = -1;
long overlap1 = -1;
long overlap2 = -1;
for (int i = 0; i < n.data.size(); i++) {
// BoundedObject node = n.data.get(i);
// BoundingBox b = node.getBounds();
BoundingBox b = cache.get(i);
long d1 = expansionNeeded(b, g1.box);
long d2 = expansionNeeded(b, g2.box);
long 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
// BoundingBox b = nmax.getBounds();
cache.remove(nmax_index);
if (overlap1 > overlap2) {
g1.data.add(nmax);
} else if (overlap2 > overlap1) {
g2.data.add(nmax);
} else {
double area1 = area(g1.box);
double area2 = area(g2.box);
if (area1 > area2) {
g2.data.add(nmax);
} else if (area2 > area1) {
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();
}
}
}
/**
* Default constructor.
*/
private RTree() {
this(2, 12);
}
/**
* 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) {
if (minChildren < 2 || minChildren > maxChildren/2)
throw new IllegalArgumentException("2 <= minChildren <= maxChildren/2");
splitter = new QuadraticNodeSplitter();
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
*/
public void query(Collection<BoundedObject> results) {
BoundingBox box;
try {
box = new BoundingBox(-BoundingBox.MAX_LON_E7, -BoundingBox.MAX_LAT_E7, BoundingBox.MAX_LON_E7, BoundingBox.MAX_LAT_E7);
query(results, box, root);
} catch (OsmException e) {
// shouldn't happen but log anyway
Log.d(DEBUG_TAG,"new BoundingBox caused " + e);
}
}
public void query(Collection<BoundedObject> results, BoundingBox box) {
query(results, box, root);
}
private void query(Collection<BoundedObject> results, BoundingBox box, Node node) {
// Log.d(DEBUG_TAG,"query called");
if (node == null) return;
if (node.isLeaf()) {
// Log.d(DEBUG_TAG,"leaf");
for (int i = 0; i < node.data.size(); i++) {
BoundedObject bo = node.data.get(i);
BoundingBox box2 = bo.getBounds();
if (!box2.isEmpty()) {
if (BoundingBox.intersects(box2, box)) {
results.add(bo);
}
} else {
// Log.d(DEBUG_TAG,"point " + box2.left + " " + box2.top + " " + box);
if (box.contains(box2.getLeft(),box2.getTop())) {
results.add(bo);
// Log.d(DEBUG_TAG,"adding " + bo);
}
}
}
} else {
// Log.d(DEBUG_TAG,"not leaf");
for (int i = 0; i < node.children.size(); i++) {
if (BoundingBox.intersects(node.children.get(i).box, box)) {
query(results, box, node.children.get(i));
}
}
}
}
/**
* Returns one item that intersects the query box, or null if nothing intersects
* the query box.
*/
public BoundedObject queryOne(BoundingBox box) {
return queryOne(box,root);
}
private BoundedObject queryOne(BoundingBox box, Node node) {
if (node == null) return null;
if (node.isLeaf()) {
for (int i = 0; i < node.data.size(); i++) {
BoundingBox box2 = node.data.get(i).getBounds();
if (!box2.isEmpty()) {
if (BoundingBox.intersects(box2, box)) {
return node.data.get(i);
}
} else {
if (box.contains(box2.getLeft(),box2.getTop())) {
return node.data.get(i);
}
}
}
return null;
} else {
for (int i = 0; i < node.children.size(); i++) {
if (BoundingBox.intersects(node.children.get(i).box, box)) {
BoundedObject result = queryOne(box,node.children.get(i));
if (result != null) return result;
}
}
return null;
}
}
/**
* Returns items whose Rect contains the specified point.
* @param results A collection to store the query results.
* @param px Point X coordinate
* @param py Point Y coordinate
*/
public void query(Collection<? super BoundedObject> results, int px, int py) {
query(results, px, py, root);
}
private void query(Collection<? super BoundedObject> results, int px, int py, Node node) {
if (node == null) return;
if (node.isLeaf()) {
for (int i = 0; i < node.data.size(); i++) {
BoundingBox b = node.data.get(i).getBounds();
if (b.isEmpty()) {
if (b.getLeft() == px && b.getTop() == py) {
results.add(node.data.get(i));
}
} else {
if (b.contains(px, py)) {
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)) {
query(results, px, py, 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) {
return queryOne(px, py, root);
}
private BoundedObject queryOne(int px, int py, 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)) {
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)) {
BoundedObject result = queryOne(px, py, node.children.get(i));
if (result != null) return result;
}
}
return null;
}
}
/**
* Removes the specified object if it is in the tree.
* @param o
*/
public synchronized void remove(BoundedObject o) {
Node n = chooseLeaf(o.getBounds(), root);
// assert(n.isLeaf());
n.data.remove(o);
n.computeMBR();
}
/**
* Return true if o is in the tree.
* @param o
*/
public synchronized boolean contains(BoundedObject o) {
Node n = chooseLeaf(o.getBounds(), root);
// assert(n.isLeaf());
return n.data.contains(o);
}
/**
* Inserts object o into the tree. Note that if the value of o.getBounds() changes
* while in the R-tree, the result is undefined.
* @throws NullPointerException If o == null
*/
public synchronized 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.getBounds(), 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(BoundingBox box, Node n) {
// assert(n != null);
if (n.isLeaf()) {
return n;
} else {
long maxOverlap = Long.MAX_VALUE;
Node maxnode = null;
double maxnodeArea = Double.MAX_VALUE;
for (int i = 0; i < n.children.size(); i++) {
Node child = n.children.get(i);
long overlap = expansionNeeded(child.box, box);
if ((overlap < maxOverlap) || (overlap == maxOverlap)
&& area(child.box) < maxnodeArea) {
maxOverlap = overlap;
maxnode = child;
maxnodeArea = area(maxnode.box);
}
}
if (maxnode == null) // Not sure how this could occur
return null;
return chooseLeaf(box, maxnode);
}
}
/**
* Returns the amount that other will need to be expanded to fit this.
*/
private static long expansionNeeded(BoundingBox one, BoundingBox two) {
long total = 0;
int twoL = two.getLeft();
int oneL = one.getLeft();
if(twoL < oneL) total += (long)oneL - (long)twoL;
int twoR = two.getRight();
int oneR = one.getRight();
if( twoR > oneR) total += (long)twoR - (long)oneR;
int twoT = two.getTop();
int oneT = one.getTop();
if(twoT < oneT) total += (long)oneT - (long)twoT;
int twoB = two.getBottom();
int oneB = one.getBottom();
if(twoB > oneB) total += (long)twoB - (long)oneB;
return total;
}
private static double area(BoundingBox box) {
return (double)box.getWidth() * (double)box.getHeight();
}
/**
* Find an object in the tree without using bounding boxes
* @param o
* @param node
* @param level
* @return
*/
public void debug(BoundedObject o) {
System.out.println("debug: target bounding box " + o.getBounds());
debug(o,root,0);
}
private boolean debug(BoundedObject o, Node node,int level) {
if (node == null) {
System.out.println(level + " debug: node is null");
return false;
}
BoundingBox box = o.getBounds();
if (node.isLeaf()) {
for (int i = 0; i < node.data.size(); i++) {
if (node.data.get(i)==o) {
BoundingBox box2 = node.data.get(i).getBounds();
System.out.println(level + " debug: found object parent box " + node.box);
System.out.println(level + " debug: would have matched correctly: 1: " + box2.contains(box.getRight(),box.getTop()));
if (!box2.isEmpty()) {
System.out.println(level + " debug: would have matched correctly: 2: " + BoundingBox.intersects(box2, box));
} else {
System.out.println(level + " debug: would have matched correctly: 3: " +box.contains(box2.getLeft(),box2.getTop()));
}
return true;
}
}
} else {
for (int i = 0; i < node.children.size(); i++) { // this is what should normally happen
if (BoundingBox.intersects(node.children.get(i).box, box)) {
if (debug(o,node.children.get(i), level+1)) {
System.out.println(level + " debug: target box intersects with node box");
return true;
}
}
}
for (int i = 0; i < node.children.size(); i++) {
if (!BoundingBox.intersects(node.children.get(i).box, o.getBounds())) {
if (node.children.get(i).box.contains(box.getLeft(),box.getBottom())) {
if (debug(o,node.children.get(i), level+1)) {
System.out.println(level + " debug: target point contained in node box, didn't intersect with " + node.children.get(i).box);
return true;
}
} else {
if (debug(o,node.children.get(i), level+1)) {
System.out.println(level + " debug: target box doesn't intersect with node box " + node.children.get(i).box);
return true;
}
}
}
}
}
return false;
}
}