package rescuecore2.misc.geometry.spatialindex; import java.util.Collection; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.Stack; import rescuecore2.log.Logger; /** A spatial index that creates a (probably) unbalanced tree of bounding boxes. This is almost certainly not as efficient as an RTree or similar, but it is much easier to implement. */ public class BBTree extends AbstractSpatialIndex { private static final int DEFAULT_MAX_CHILDREN = 3; private static final int DEFAULT_TIMING_POINTS = 50000; private static final int DEFAULT_TIMING_LINES = 50000; private static final int DEFAULT_TIMING_REGIONS = 1000; private Node root; private int maxChildren; /** Construct a BBTree with a default maximum number of children per branch. */ public BBTree() { this(DEFAULT_MAX_CHILDREN); } /** Construct a BBTree with a given maximum number of children per branch. @param maxChildren The maximum number of children per branch. */ public BBTree(int maxChildren) { this.maxChildren = maxChildren; root = new Branch(); } /** Conduct a timing test. @param args Command line arguments: [-p points] [-l lines] [-r regions] */ public static void main(String[] args) { int points = DEFAULT_TIMING_POINTS; int lines = DEFAULT_TIMING_LINES; int regions = DEFAULT_TIMING_REGIONS; // CHECKSTYLE:OFF:ModifiedControlVariable for (int i = 0; i < args.length; ++i) { if ("-p".equalsIgnoreCase(args[i])) { points = Integer.parseInt(args[++i]); } else if ("-l".equalsIgnoreCase(args[i])) { lines = Integer.parseInt(args[++i]); } else if ("-r".equalsIgnoreCase(args[i])) { regions = Integer.parseInt(args[++i]); } else { System.out.println("Unrecognised option: " + args[i]); } } // CHECKSTYLE:ON:ModifiedControlVariable BBTree tree = new BBTree(); long start = System.currentTimeMillis(); for (int i = 0; i < points; ++i) { rescuecore2.misc.geometry.Point2D p = new rescuecore2.misc.geometry.Point2D(Math.random(), Math.random()); tree.insert(p); } for (int i = 0; i < lines; ++i) { rescuecore2.misc.geometry.Point2D p1 = new rescuecore2.misc.geometry.Point2D(Math.random(), Math.random()); rescuecore2.misc.geometry.Point2D p2 = new rescuecore2.misc.geometry.Point2D(Math.random(), Math.random()); rescuecore2.misc.geometry.Line2D l = new rescuecore2.misc.geometry.Line2D(p1, p2); tree.insert(l); } tree.logTree(); long fill = System.currentTimeMillis(); for (int i = 0; i < regions; ++i) { double xMin = Math.random(); double yMin = Math.random(); double xMax = Math.random(); double yMax = Math.random(); tree.getItemsInRegion(Math.min(xMin, xMax), Math.min(yMin, yMax), Math.max(xMin, xMax), Math.max(yMin, yMax)); } long end = System.currentTimeMillis(); long fillTime = fill - start; long fetchTime = end - fill; double fillAverage = ((double)fillTime) / (double)(points + lines); double fetchAverage = ((double)fetchTime) / (double)regions; System.out.println("Time to populate tree with " + points + " points and " + lines + " lines: " + fillTime + "ms (average " + fillAverage + "ms)"); System.out.println("Time to read " + regions + " regions: " + fetchTime + "ms (average " + fetchAverage + "ms)"); } @Override public void insert(Indexable i) { // Logger.debug("Inserting " + i); // Logger.debug("Tree before insert"); // logTree(); Leaf newLeaf = new Leaf(i); Node insertPoint = findInsertionPoint(root, i.getBoundingRegion()); // Logger.debug("Insertion point: " + insertPoint); if (insertPoint instanceof Leaf) { Branch b = new Branch(); if (insertPoint.parent != null) { insertPoint.parent.insert(b); insertPoint.parent.remove(insertPoint); } b.insert(insertPoint); b.insert(newLeaf); } else { Branch b = (Branch)insertPoint; b.insert(newLeaf); } newLeaf.recomputeBounds(); // Logger.debug("Tree after insert"); // logTree(); } @Override public Collection<Indexable> getItemsInRegion(Region region) { // Logger.debug("Getting items in region " + region); Collection<Indexable> result = new ArrayList<Indexable>(); if (root != null) { Stack<Node> open = new Stack<Node>(); open.push(root); while (!open.isEmpty()) { Node next = open.pop(); // Logger.debug("Next node: " + next); if (next.bounds.intersects(region)) { if (next instanceof Branch) { // Logger.debug("Adding children"); open.addAll(((Branch)next).children); } else if (next instanceof Leaf) { Leaf l = (Leaf)next; if (region.intersects(l.entry.getBoundingRegion())) { // Logger.debug("Leaf intersects region"); result.add(l.entry); } /* else { Logger.debug("Leaf does not intersect region"); } */ } } /* else { Logger.debug("No intersection"); } */ } } return result; } /** Write this tree to the logger. */ public void logTree() { Logger.debug("BBTree"); Logger.debug("Max children per node: " + maxChildren); Logger.debug("Tree depth: " + root.getDepth()); root.log(" "); } private Node findInsertionPoint(Node parent, Region newRegion) { // Logger.debug("Choosing insertion point: current parent = " + parent + ", new region = " + newRegion); if (parent instanceof Leaf) { // Logger.debug("Parent is a leaf"); return parent; } Branch b = (Branch)parent; if (b.children.size() < maxChildren) { // Logger.debug("Parent can fit the child"); return b; } Node best = findLeastAreaEnlargement(b.children, newRegion); // Logger.debug("Best child: " + best); return findInsertionPoint(best, newRegion); } private Node findLeastAreaEnlargement(Collection<Node> nodes, Region newRegion) { // Logger.debug("Finding least area enlargement for " + newRegion); Node best = null; double bestDiff = 0; for (Node next : nodes) { // Logger.debug("Next node: " + next); double diff = computeAreaEnlargement(next, newRegion); if (best == null || diff < bestDiff) { best = next; bestDiff = diff; } } // Logger.debug("Best: " + best); return best; } private double computeAreaEnlargement(Node node, Region newRegion) { double oldArea = node.bounds instanceof RectangleRegion ? ((RectangleRegion)node.bounds).getArea() : 0; double newArea = cover(node.bounds, newRegion).getArea(); // Logger.debug("Old area: " + oldArea); // Logger.debug("New area: " + newArea); // Logger.debug("Increase: " + (newArea - oldArea)); return newArea - oldArea; } private RectangleRegion cover(Region... regions) { return cover(Arrays.asList(regions)); } private RectangleRegion cover(List<? extends Region> regions) { if (regions.isEmpty()) { throw new IllegalArgumentException("Cannot cover zero regions"); } double xMin = Double.POSITIVE_INFINITY; double yMin = Double.POSITIVE_INFINITY; double xMax = Double.NEGATIVE_INFINITY; double yMax = Double.NEGATIVE_INFINITY; for (Region next : regions) { // CHECKSTYLE:OFF:EmptyBlock if (next == null || next instanceof NullRegion) { // Ignore } else { xMin = Math.min(xMin, next.getXMin()); xMax = Math.max(xMax, next.getXMax()); yMin = Math.min(yMin, next.getYMin()); yMax = Math.max(yMax, next.getYMax()); } } if (Double.isInfinite(xMin)) { return null; } return new RectangleRegion(xMin, yMin, xMax, yMax); } private abstract class Node { Region bounds; Branch parent; Node() { bounds = null; parent = null; } abstract void recomputeBounds(); abstract void log(String prefix); abstract int getDepth(); } private class Branch extends Node { List<Node> children; Branch() { this.children = new ArrayList<Node>(maxChildren); } void insert(Node child) { children.add(child); child.parent = this; bounds = cover(bounds, child.bounds); } void remove(Node child) { children.remove(child); child.parent = null; } @Override public String toString() { return "Branch [" + bounds + "] (" + children.size() + " children) {depth " + getDepth() + "}"; } @Override void log(String prefix) { Logger.debug(prefix + this); String newPrefix = prefix + " "; for (Node next : children) { next.log(newPrefix); } } @Override void recomputeBounds() { List<Region> childBounds = new ArrayList<Region>(children.size()); for (Node next : children) { childBounds.add(next.bounds); } bounds = cover(childBounds); if (parent != null) { parent.recomputeBounds(); } } @Override int getDepth() { int max = 0; for (Node next : children) { max = Math.max(max, next.getDepth()); } return max + 1; } } private class Leaf extends Node { Indexable entry; Leaf(Indexable entry) { this.entry = entry; bounds = entry.getBoundingRegion(); } @Override public String toString() { return "Leaf [" + bounds + "] (" + entry + ")"; } @Override void log(String prefix) { Logger.debug(prefix + this.toString()); } @Override void recomputeBounds() { if (parent != null) { parent.recomputeBounds(); } } @Override int getDepth() { return 1; } } }