package iamrescue.agent.firebrigade.util;
// RTree.java
// Java Spatial Index Library
// Copyright (C) 2002 Infomatiq Limited
// Copyright (C) 2008 Aled Morris <aled@sourceforge.net>
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import gnu.trove.TIntArrayList;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntProcedure;
import gnu.trove.TIntStack;
import java.awt.geom.Line2D;
import java.util.Properties;
import org.apache.log4j.Logger;
import com.infomatiq.jsi.IntProcedure;
import com.infomatiq.jsi.Point;
import com.infomatiq.jsi.Rectangle;
import com.infomatiq.jsi.SpatialIndex;
/**
* <p>
* This is a lightweight RTree implementation, specifically designed for the
* following features (in order of importance):
* <ul>
* <li>Fast intersection query performance. To achieve this, the RTree uses only
* main memory to store entries. Obviously this will only improve performance if
* there is enough physical memory to avoid paging.</li>
* <li>Low memory requirements.</li>
* <li>Fast add performance.</li>
* </ul>
* </p>
*
* <p>
* The main reason for the high speed of this RTree implementation is the
* avoidance of the creation of unnecessary objects, mainly achieved by using
* primitive collections from the trove4j library.
* </p>
*
* RS: Extended class for line/object intersection
*
* @author aled@sourceforge.net
* @author Ruben Stranders
* @version 1.0b2p1
*/
public class RTree implements SpatialIndex {
private static final Logger log = Logger.getLogger(RTree.class.getName());
private static final Logger deleteLog = Logger.getLogger(RTree.class
.getName()
+ "-delete");
private static final String version = "1.0b2p1";
// parameters of the tree
private final static int DEFAULT_MAX_NODE_ENTRIES = 10;
int maxNodeEntries;
int minNodeEntries;
// map of nodeId -> node object
// [x] TODO eliminate this map - it should not be needed. Nodes
// can be found by traversing the tree.
private TIntObjectHashMap nodeMap = new TIntObjectHashMap();
// internal consistency checking - set to true if debugging tree corruption
private final static boolean INTERNAL_CONSISTENCY_CHECKING = false;
// used to mark the status of entries during a node split
private final static int ENTRY_STATUS_ASSIGNED = 0;
private final static int ENTRY_STATUS_UNASSIGNED = 1;
private byte[] entryStatus = null;
private byte[] initialEntryStatus = null;
// stacks used to store nodeId and entry index of each node
// from the root down to the leaf. Enables fast lookup
// of nodes when a split is propagated up the tree.
private TIntStack parents = new TIntStack();
private TIntStack parentsEntry = new TIntStack();
// initialisation
private int treeHeight = 1; // leaves are always level 1
private int rootNodeId = 0;
private int size = 0;
// Enables creation of new nodes
private int highestUsedNodeId = rootNodeId;
// Deleted node objects are retained in the nodeMap,
// so that they can be reused. Store the IDs of nodes
// which can be reused.
private TIntStack deletedNodeIds = new TIntStack();
// List of nearest rectangles. Use a member variable to
// avoid recreating the object each time nearest() is called.
private TIntArrayList nearestIds = new TIntArrayList();
// Inner class used as a bridge between the trove4j TIntProcedure
// and the SpatialIndex IntProcedure. This is used because
// the nearest rectangles must be stored as they are found, in
// case a closer one is found later.
//
// A single instance of this class is used to avoid creating a new
// one every time nearest() is called.
private class TIntProcedureVisit implements TIntProcedure {
public IntProcedure m_intProcedure = null;
public void setProcedure(IntProcedure ip) {
m_intProcedure = ip;
}
public boolean execute(int i) {
m_intProcedure.execute(i);
return true;
}
};
private TIntProcedureVisit visitProc = new TIntProcedureVisit();
/**
* Constructor. Use init() method to initialize parameters of the RTree.
*/
public RTree() {
return; // NOP
}
// -------------------------------------------------------------------------
// public implementation of SpatialIndex interface:
// init(Properties)
// add(Rectangle, int)
// delete(Rectangle, int)
// nearest(Point, IntProcedure, float)
// intersects(Rectangle, IntProcedure)
// contains(Rectangle, IntProcedure)
// size()
// -------------------------------------------------------------------------
/**
* <p>
* Initialize implementation dependent properties of the RTree. Currently
* implemented properties are:
* <ul>
* <li>MaxNodeEntries</li> This specifies the maximum number of entries in a
* node. The default value is 10, which is used if the property is not
* specified, or is less than 2.
* <li>MinNodeEntries</li> This specifies the minimum number of entries in a
* node. The default value is half of the MaxNodeEntries value (rounded
* down), which is used if the property is not specified or is less than 1.
* </ul>
* </p>
*
* @see com.infomatiq.jsi.SpatialIndex#init(Properties)
*/
public void init(Properties props) {
maxNodeEntries = Integer.parseInt(props.getProperty("MaxNodeEntries",
"0"));
minNodeEntries = Integer.parseInt(props.getProperty("MinNodeEntries",
"0"));
// Obviously a node with less than 2 entries cannot be split.
// The node splitting algorithm will work with only 2 entries
// per node, but will be inefficient.
if (maxNodeEntries < 2) {
log.warn("Invalid MaxNodeEntries = " + maxNodeEntries
+ " Resetting to default value of "
+ DEFAULT_MAX_NODE_ENTRIES);
maxNodeEntries = DEFAULT_MAX_NODE_ENTRIES;
}
// The MinNodeEntries must be less than or equal to (int)
// (MaxNodeEntries / 2)
if (minNodeEntries < 1 || minNodeEntries > maxNodeEntries / 2) {
log.warn("MinNodeEntries must be between 1 and MaxNodeEntries / 2");
minNodeEntries = maxNodeEntries / 2;
}
entryStatus = new byte[maxNodeEntries];
initialEntryStatus = new byte[maxNodeEntries];
for (int i = 0; i < maxNodeEntries; i++) {
initialEntryStatus[i] = ENTRY_STATUS_UNASSIGNED;
}
Node root = new Node(rootNodeId, 1, maxNodeEntries);
nodeMap.put(rootNodeId, root);
log.info("init() " + " MaxNodeEntries = " + maxNodeEntries
+ ", MinNodeEntries = " + minNodeEntries);
}
/**
* @see com.infomatiq.jsi.SpatialIndex#add(Rectangle, int)
*/
public void add(Rectangle r, int id) {
if (log.isDebugEnabled()) {
log.debug("Adding rectangle " + r + ", id " + id);
}
add(r.copy(), id, 1);
size++;
}
/**
* Adds a new entry at a specified level in the tree
*/
private void add(Rectangle r, int id, int level) {
// I1 [Find position for new record] Invoke ChooseLeaf to select a
// leaf node L in which to place r
Node n = chooseNode(r, level);
Node newLeaf = null;
// I2 [Add record to leaf node] If L has room for another entry,
// install E. Otherwise invoke SplitNode to obtain L and LL containing
// E and all the old entries of L
if (n.entryCount < maxNodeEntries) {
n.addEntryNoCopy(r, id);
} else {
newLeaf = splitNode(n, r, id);
}
// I3 [Propagate changes upwards] Invoke AdjustTree on L, also passing
// LL
// if a split was performed
Node newNode = adjustTree(n, newLeaf);
// I4 [Grow tree taller] If node split propagation caused the root to
// split, create a new root whose children are the two resulting nodes.
if (newNode != null) {
int oldRootNodeId = rootNodeId;
Node oldRoot = getNode(oldRootNodeId);
rootNodeId = getNextNodeId();
treeHeight++;
Node root = new Node(rootNodeId, treeHeight, maxNodeEntries);
root.addEntry(newNode.mbr, newNode.nodeId);
root.addEntry(oldRoot.mbr, oldRoot.nodeId);
nodeMap.put(rootNodeId, root);
}
if (INTERNAL_CONSISTENCY_CHECKING) {
checkConsistency(rootNodeId, treeHeight, null);
}
}
/**
* @see com.infomatiq.jsi.SpatialIndex#delete(Rectangle, int)
*/
public boolean delete(Rectangle r, int id) {
// FindLeaf algorithm inlined here. Note the "official" algorithm
// searches all overlapping entries. This seems inefficient to me,
// as an entry is only worth searching if it contains (NOT overlaps)
// the rectangle we are searching for.
//
// Also the algorithm has been changed so that it is not recursive.
// FL1 [Search subtrees] If root is not a leaf, check each entry
// to determine if it contains r. For each entry found, invoke
// findLeaf on the node pointed to by the entry, until r is found or
// all entries have been checked.
parents.clear();
parents.push(rootNodeId);
parentsEntry.clear();
parentsEntry.push(-1);
Node n = null;
int foundIndex = -1; // index of entry to be deleted in leaf
while (foundIndex == -1 && parents.size() > 0) {
n = getNode(parents.peek());
int startIndex = parentsEntry.peek() + 1;
if (!n.isLeaf()) {
deleteLog.debug("searching node " + n.nodeId + ", from index "
+ startIndex);
boolean contains = false;
for (int i = startIndex; i < n.entryCount; i++) {
if (n.entries[i].contains(r)) {
parents.push(n.ids[i]);
parentsEntry.pop();
parentsEntry.push(i); // this becomes the start index
// when the child has been
// searched
parentsEntry.push(-1);
contains = true;
break; // ie go to next iteration of while()
}
}
if (contains) {
continue;
}
} else {
foundIndex = n.findEntry(r, id);
}
parents.pop();
parentsEntry.pop();
} // while not found
if (foundIndex != -1) {
n.deleteEntry(foundIndex, minNodeEntries);
condenseTree(n);
size--;
}
// shrink the tree if possible (i.e. if root node has exactly one
// entry,and that
// entry is not a leaf node, delete the root (it's entry becomes the new
// root)
Node root = getNode(rootNodeId);
while (root.entryCount == 1 && treeHeight > 1) {
root.entryCount = 0;
rootNodeId = root.ids[0];
treeHeight--;
root = getNode(rootNodeId);
}
return (foundIndex != -1);
}
/**
* @see com.infomatiq.jsi.SpatialIndex#nearest(Point, IntProcedure, float)
*/
public void nearest(Point p, IntProcedure v, float furthestDistance) {
Node rootNode = getNode(rootNodeId);
nearest(p, rootNode, furthestDistance);
visitProc.setProcedure(v);
nearestIds.forEach(visitProc);
nearestIds.clear();
}
/**
* @see com.infomatiq.jsi.SpatialIndex#intersects(Rectangle, IntProcedure)
*/
public void intersects(Rectangle r, IntProcedure v) {
Node rootNode = getNode(rootNodeId);
intersects(r, v, rootNode);
}
/**
* @see com.infomatiq.jsi.SpatialIndex#contains(Rectangle, IntProcedure)
*/
public void contains(Rectangle r, IntProcedure v) {
// find all rectangles in the tree that are contained by the passed
// rectangle
// written to be non-recursive (should model other searches on this?)
parents.clear();
parents.push(rootNodeId);
parentsEntry.clear();
parentsEntry.push(-1);
// TODO: possible shortcut here - could test for intersection with the
// MBR of the root node. If no intersection, return immediately.
while (parents.size() > 0) {
Node n = getNode(parents.peek());
int startIndex = parentsEntry.peek() + 1;
if (!n.isLeaf()) {
// go through every entry in the index node to check
// if it intersects the passed rectangle. If so, it
// could contain entries that are contained.
boolean intersects = false;
for (int i = startIndex; i < n.entryCount; i++) {
if (r.intersects(n.entries[i])) {
parents.push(n.ids[i]);
parentsEntry.pop();
parentsEntry.push(i); // this becomes the start index
// when the child has been
// searched
parentsEntry.push(-1);
intersects = true;
break; // ie go to next iteration of while()
}
}
if (intersects) {
continue;
}
} else {
// go through every entry in the leaf to check if
// it is contained by the passed rectangle
for (int i = 0; i < n.entryCount; i++) {
if (r.contains(n.entries[i])) {
v.execute(n.ids[i]);
}
}
}
parents.pop();
parentsEntry.pop();
}
}
/**
* @see com.infomatiq.jsi.SpatialIndex#size()
*/
public int size() {
return size;
}
/**
* @see com.infomatiq.jsi.SpatialIndex#getBounds()
*/
public Rectangle getBounds() {
Rectangle bounds = null;
Node n = getNode(getRootNodeId());
if (n != null && n.getMBR() != null) {
bounds = n.getMBR().copy();
}
return bounds;
}
/**
* @see com.infomatiq.jsi.SpatialIndex#getVersion()
*/
public String getVersion() {
return "RTree-" + version;
}
// -------------------------------------------------------------------------
// end of SpatialIndex methods
// -------------------------------------------------------------------------
/**
* Get the next available node ID. Reuse deleted node IDs if possible
*/
private int getNextNodeId() {
int nextNodeId = 0;
if (deletedNodeIds.size() > 0) {
nextNodeId = deletedNodeIds.pop();
} else {
nextNodeId = 1 + highestUsedNodeId++;
}
return nextNodeId;
}
/**
* Get a node object, given the ID of the node.
*/
public Node getNode(int index) {
return (Node) nodeMap.get(index);
}
/**
* Get the highest used node ID
*/
public int getHighestUsedNodeId() {
return highestUsedNodeId;
}
/**
* Get the root node ID
*/
public int getRootNodeId() {
return rootNodeId;
}
/**
* Split a node. Algorithm is taken pretty much verbatim from Guttman's
* original paper.
*
* @return new node object.
*/
private Node splitNode(Node n, Rectangle newRect, int newId) {
// [Pick first entry for each group] Apply algorithm pickSeeds to
// choose two entries to be the first elements of the groups. Assign
// each to a group.
// debug code
float initialArea = 0;
if (log.isDebugEnabled()) {
Rectangle union = n.mbr.union(newRect);
initialArea = union.area();
}
System.arraycopy(initialEntryStatus, 0, entryStatus, 0, maxNodeEntries);
Node newNode = null;
newNode = new Node(getNextNodeId(), n.level, maxNodeEntries);
nodeMap.put(newNode.nodeId, newNode);
pickSeeds(n, newRect, newId, newNode); // this also sets the entryCount
// to 1
// [Check if done] If all entries have been assigned, stop. If one
// group has so few entries that all the rest must be assigned to it in
// order for it to have the minimum number m, assign them and stop.
while (n.entryCount + newNode.entryCount < maxNodeEntries + 1) {
if (maxNodeEntries + 1 - newNode.entryCount == minNodeEntries) {
// assign all remaining entries to original node
for (int i = 0; i < maxNodeEntries; i++) {
if (entryStatus[i] == ENTRY_STATUS_UNASSIGNED) {
entryStatus[i] = ENTRY_STATUS_ASSIGNED;
n.mbr.add(n.entries[i]);
n.entryCount++;
}
}
break;
}
if (maxNodeEntries + 1 - n.entryCount == minNodeEntries) {
// assign all remaining entries to new node
for (int i = 0; i < maxNodeEntries; i++) {
if (entryStatus[i] == ENTRY_STATUS_UNASSIGNED) {
entryStatus[i] = ENTRY_STATUS_ASSIGNED;
newNode.addEntryNoCopy(n.entries[i], n.ids[i]);
n.entries[i] = null;
}
}
break;
}
// [Select entry to assign] Invoke algorithm pickNext to choose the
// next entry to assign. Add it to the group whose covering
// rectangle
// will have to be enlarged least to accommodate it. Resolve ties
// by adding the entry to the group with smaller area, then to the
// the one with fewer entries, then to either. Repeat from S2
pickNext(n, newNode);
}
n.reorganize(this);
// check that the MBR stored for each node is correct.
if (INTERNAL_CONSISTENCY_CHECKING) {
if (!n.mbr.equals(calculateMBR(n))) {
log.error("Error: splitNode old node MBR wrong");
}
if (!newNode.mbr.equals(calculateMBR(newNode))) {
log.error("Error: splitNode new node MBR wrong");
}
}
// debug code
if (log.isDebugEnabled()) {
float newArea = n.mbr.area() + newNode.mbr.area();
float percentageIncrease = (100 * (newArea - initialArea))
/ initialArea;
log.debug("Node " + n.nodeId + " split. New area increased by "
+ percentageIncrease + "%");
}
return newNode;
}
/**
* Pick the seeds used to split a node. Select two entries to be the first
* elements of the groups
*/
private void pickSeeds(Node n, Rectangle newRect, int newId, Node newNode) {
// Find extreme rectangles along all dimension. Along each dimension,
// find the entry whose rectangle has the highest low side, and the one
// with the lowest high side. Record the separation.
float maxNormalizedSeparation = 0;
int highestLowIndex = 0;
int lowestHighIndex = 0;
// for the purposes of picking seeds, take the MBR of the node to
// include
// the new rectangle aswell.
n.mbr.add(newRect);
if (log.isDebugEnabled()) {
log.debug("pickSeeds(): NodeId = " + n.nodeId + ", newRect = "
+ newRect);
}
for (int d = 0; d < Rectangle.DIMENSIONS; d++) {
float tempHighestLow = newRect.min[d];
int tempHighestLowIndex = -1; // -1 indicates the new rectangle is
// the seed
float tempLowestHigh = newRect.max[d];
int tempLowestHighIndex = -1;
for (int i = 0; i < n.entryCount; i++) {
float tempLow = n.entries[i].min[d];
if (tempLow >= tempHighestLow) {
tempHighestLow = tempLow;
tempHighestLowIndex = i;
} else { // ensure that the same index cannot be both lowestHigh
// and highestLow
float tempHigh = n.entries[i].max[d];
if (tempHigh <= tempLowestHigh) {
tempLowestHigh = tempHigh;
tempLowestHighIndex = i;
}
}
// PS2 [Adjust for shape of the rectangle cluster] Normalize the
// separations
// by dividing by the widths of the entire set along the
// corresponding
// dimension
float normalizedSeparation = (tempHighestLow - tempLowestHigh)
/ (n.mbr.max[d] - n.mbr.min[d]);
if (normalizedSeparation > 1 || normalizedSeparation < -1) {
log.error("Invalid normalized separation");
}
if (log.isDebugEnabled()) {
log.debug("Entry " + i + ", dimension " + d
+ ": HighestLow = " + tempHighestLow + " (index "
+ tempHighestLowIndex + ")" + ", LowestHigh = "
+ tempLowestHigh + " (index " + tempLowestHighIndex
+ ", NormalizedSeparation = "
+ normalizedSeparation);
}
// PS3 [Select the most extreme pair] Choose the pair with the
// greatest
// normalized separation along any dimension.
if (normalizedSeparation > maxNormalizedSeparation) {
maxNormalizedSeparation = normalizedSeparation;
highestLowIndex = tempHighestLowIndex;
lowestHighIndex = tempLowestHighIndex;
}
}
}
// highestLowIndex is the seed for the new node.
if (highestLowIndex == -1) {
newNode.addEntry(newRect, newId);
} else {
newNode.addEntryNoCopy(n.entries[highestLowIndex],
n.ids[highestLowIndex]);
n.entries[highestLowIndex] = null;
// move the new rectangle into the space vacated by the seed for the
// new node
n.entries[highestLowIndex] = newRect;
n.ids[highestLowIndex] = newId;
}
// lowestHighIndex is the seed for the original node.
if (lowestHighIndex == -1) {
lowestHighIndex = highestLowIndex;
}
entryStatus[lowestHighIndex] = ENTRY_STATUS_ASSIGNED;
n.entryCount = 1;
n.mbr.set(n.entries[lowestHighIndex].min,
n.entries[lowestHighIndex].max);
}
/**
* Pick the next entry to be assigned to a group during a node split.
*
* [Determine cost of putting each entry in each group] For each entry not
* yet in a group, calculate the area increase required in the covering
* rectangles of each group
*/
private int pickNext(Node n, Node newNode) {
float maxDifference = Float.NEGATIVE_INFINITY;
int next = 0;
int nextGroup = 0;
maxDifference = Float.NEGATIVE_INFINITY;
if (log.isDebugEnabled()) {
log.debug("pickNext()");
}
for (int i = 0; i < maxNodeEntries; i++) {
if (entryStatus[i] == ENTRY_STATUS_UNASSIGNED) {
if (n.entries[i] == null) {
log.error("Error: Node " + n.nodeId + ", entry " + i
+ " is null");
}
float nIncrease = n.mbr.enlargement(n.entries[i]);
float newNodeIncrease = newNode.mbr.enlargement(n.entries[i]);
float difference = Math.abs(nIncrease - newNodeIncrease);
if (difference > maxDifference) {
next = i;
if (nIncrease < newNodeIncrease) {
nextGroup = 0;
} else if (newNodeIncrease < nIncrease) {
nextGroup = 1;
} else if (n.mbr.area() < newNode.mbr.area()) {
nextGroup = 0;
} else if (newNode.mbr.area() < n.mbr.area()) {
nextGroup = 1;
} else if (newNode.entryCount < maxNodeEntries / 2) {
nextGroup = 0;
} else {
nextGroup = 1;
}
maxDifference = difference;
}
if (log.isDebugEnabled()) {
log.debug("Entry " + i + " group0 increase = " + nIncrease
+ ", group1 increase = " + newNodeIncrease
+ ", diff = " + difference + ", MaxDiff = "
+ maxDifference + " (entry " + next + ")");
}
}
}
entryStatus[next] = ENTRY_STATUS_ASSIGNED;
if (nextGroup == 0) {
n.mbr.add(n.entries[next]);
n.entryCount++;
} else {
// move to new node.
newNode.addEntryNoCopy(n.entries[next], n.ids[next]);
n.entries[next] = null;
}
return next;
}
/**
* Recursively searches the tree for the nearest entry. Other queries call
* execute() on an IntProcedure when a matching entry is found; however
* nearest() must store the entry Ids as it searches the tree, in case a
* nearer entry is found. Uses the member variable nearestIds to store the
* nearest entry IDs.
*
* [x] TODO rewrite this to be non-recursive?
*/
private float nearest(Point p, Node n, float nearestDistance) {
for (int i = 0; i < n.entryCount; i++) {
float tempDistance = n.entries[i].distance(p);
if (n.isLeaf()) { // for leaves, the distance is an actual nearest
// distance
if (tempDistance < nearestDistance) {
nearestDistance = tempDistance;
nearestIds.clear();
}
if (tempDistance <= nearestDistance) {
nearestIds.add(n.ids[i]);
}
} else { // for index nodes, only go into them if they potentially
// could have
// a rectangle nearer than actualNearest
if (tempDistance <= nearestDistance) {
// search the child node
nearestDistance = nearest(p, getNode(n.ids[i]),
nearestDistance);
}
}
}
return nearestDistance;
}
/**
* Recursively searches the tree for all intersecting entries. Immediately
* calls execute() on the passed IntProcedure when a matching entry is
* found.
*
* [x] TODO rewrite this to be non-recursive? Make sure it doesn't slow it
* down.
*/
private void intersects(Rectangle r, IntProcedure v, Node n) {
for (int i = 0; i < n.entryCount; i++) {
if (r.intersects(n.entries[i])) {
if (n.isLeaf()) {
v.execute(n.ids[i]);
} else {
Node childNode = getNode(n.ids[i]);
intersects(r, v, childNode);
}
}
}
}
/**
* Used by delete(). Ensures that all nodes from the passed node up to the
* root have the minimum number of entries.
*
* Note that the parent and parentEntry stacks are expected to contain the
* nodeIds of all parents up to the root.
*/
private Rectangle oldRectangle = new Rectangle(0, 0, 0, 0);
private void condenseTree(Node l) {
// CT1 [Initialize] Set n=l. Set the list of eliminated
// nodes to be empty.
Node n = l;
Node parent = null;
int parentEntry = 0;
TIntStack eliminatedNodeIds = new TIntStack();
// CT2 [Find parent entry] If N is the root, go to CT6. Otherwise
// let P be the parent of N, and let En be N's entry in P
while (n.level != treeHeight) {
parent = getNode(parents.pop());
parentEntry = parentsEntry.pop();
// CT3 [Eliminiate under-full node] If N has too few entries,
// delete En from P and add N to the list of eliminated nodes
if (n.entryCount < minNodeEntries) {
parent.deleteEntry(parentEntry, minNodeEntries);
eliminatedNodeIds.push(n.nodeId);
} else {
// CT4 [Adjust covering rectangle] If N has not been eliminated,
// adjust EnI to tightly contain all entries in N
if (!n.mbr.equals(parent.entries[parentEntry])) {
oldRectangle.set(parent.entries[parentEntry].min,
parent.entries[parentEntry].max);
parent.entries[parentEntry].set(n.mbr.min, n.mbr.max);
parent.recalculateMBR(oldRectangle);
}
}
// CT5 [Move up one level in tree] Set N=P and repeat from CT2
n = parent;
}
// CT6 [Reinsert orphaned entries] Reinsert all entries of nodes in set
// Q.
// Entries from eliminated leaf nodes are reinserted in tree leaves as
// in
// Insert(), but entries from higher level nodes must be placed higher
// in
// the tree, so that leaves of their dependent subtrees will be on the
// same
// level as leaves of the main tree
while (eliminatedNodeIds.size() > 0) {
Node e = getNode(eliminatedNodeIds.pop());
for (int j = 0; j < e.entryCount; j++) {
add(e.entries[j], e.ids[j], e.level);
e.entries[j] = null;
}
e.entryCount = 0;
deletedNodeIds.push(e.nodeId);
}
}
/**
* Used by add(). Chooses a leaf to add the rectangle to.
*/
private Node chooseNode(Rectangle r, int level) {
// CL1 [Initialize] Set N to be the root node
Node n = getNode(rootNodeId);
parents.clear();
parentsEntry.clear();
// CL2 [Leaf check] If N is a leaf, return N
while (true) {
if (n == null) {
log.error("Could not get root node (" + rootNodeId + ")");
}
if (n.level == level) {
return n;
}
// CL3 [Choose subtree] If N is not at the desired level, let F be
// the entry in N
// whose rectangle FI needs least enlargement to include EI. Resolve
// ties by choosing the entry with the rectangle of smaller area.
float leastEnlargement = n.getEntry(0).enlargement(r);
int index = 0; // index of rectangle in subtree
for (int i = 1; i < n.entryCount; i++) {
Rectangle tempRectangle = n.getEntry(i);
float tempEnlargement = tempRectangle.enlargement(r);
if ((tempEnlargement < leastEnlargement)
|| ((tempEnlargement == leastEnlargement) && (tempRectangle
.area() < n.getEntry(index).area()))) {
index = i;
leastEnlargement = tempEnlargement;
}
}
parents.push(n.nodeId);
parentsEntry.push(index);
// CL4 [Descend until a leaf is reached] Set N to be the child node
// pointed to by Fp and repeat from CL2
n = getNode(n.ids[index]);
}
}
/**
* Ascend from a leaf node L to the root, adjusting covering rectangles and
* propagating node splits as necessary.
*/
private Node adjustTree(Node n, Node nn) {
// AT1 [Initialize] Set N=L. If L was split previously, set NN to be
// the resulting second node.
// AT2 [Check if done] If N is the root, stop
while (n.level != treeHeight) {
// AT3 [Adjust covering rectangle in parent entry] Let P be the
// parent
// node of N, and let En be N's entry in P. Adjust EnI so that it
// tightly
// encloses all entry rectangles in N.
Node parent = getNode(parents.pop());
int entry = parentsEntry.pop();
if (parent.ids[entry] != n.nodeId) {
log.error("Error: entry " + entry + " in node " + parent.nodeId
+ " should point to node " + n.nodeId
+ "; actually points to node " + parent.ids[entry]);
}
if (!parent.entries[entry].equals(n.mbr)) {
parent.entries[entry].set(n.mbr.min, n.mbr.max);
parent.mbr.set(parent.entries[0].min, parent.entries[0].max);
for (int i = 1; i < parent.entryCount; i++) {
parent.mbr.add(parent.entries[i]);
}
}
// AT4 [Propagate node split upward] If N has a partner NN resulting
// from
// an earlier split, create a new entry Enn with Ennp pointing to NN
// and
// Enni enclosing all rectangles in NN. Add Enn to P if there is
// room.
// Otherwise, invoke splitNode to produce P and PP containing Enn
// and
// all P's old entries.
Node newNode = null;
if (nn != null) {
if (parent.entryCount < maxNodeEntries) {
parent.addEntry(nn.mbr, nn.nodeId);
} else {
newNode = splitNode(parent, nn.mbr.copy(), nn.nodeId);
}
}
// AT5 [Move up to next level] Set N = P and set NN = PP if a split
// occurred. Repeat from AT2
n = parent;
nn = newNode;
parent = null;
newNode = null;
}
return nn;
}
/**
* Check the consistency of the tree.
*/
private void checkConsistency(int nodeId, int expectedLevel,
Rectangle expectedMBR) {
// go through the tree, and check that the internal data structures of
// the tree are not corrupted.
Node n = getNode(nodeId);
if (n == null) {
log.error("Error: Could not read node " + nodeId);
}
if (n.level != expectedLevel) {
log.error("Error: Node " + nodeId + ", expected level "
+ expectedLevel + ", actual level " + n.level);
}
Rectangle calculatedMBR = calculateMBR(n);
if (!n.mbr.equals(calculatedMBR)) {
log.error("Error: Node " + nodeId
+ ", calculated MBR does not equal stored MBR");
}
if (expectedMBR != null && !n.mbr.equals(expectedMBR)) {
log.error("Error: Node " + nodeId
+ ", expected MBR (from parent) does not equal stored MBR");
}
// Check for corruption where a parent entry is the same object as the
// child MBR
if (expectedMBR != null && n.mbr.sameObject(expectedMBR)) {
log.error("Error: Node " + nodeId
+ " MBR using same rectangle object as parent's entry");
}
for (int i = 0; i < n.entryCount; i++) {
if (n.entries[i] == null) {
log
.error("Error: Node " + nodeId + ", Entry " + i
+ " is null");
}
if (n.level > 1) { // if not a leaf
checkConsistency(n.ids[i], n.level - 1, n.entries[i]);
}
}
}
/**
* Given a node object, calculate the node MBR from it's entries. Used in
* consistency checking
*/
private Rectangle calculateMBR(Node n) {
Rectangle mbr = new Rectangle(n.entries[0].min, n.entries[0].max);
for (int i = 1; i < n.entryCount; i++) {
mbr.add(n.entries[i]);
}
return mbr;
}
/**
* Computes intersections with line and rectangles in the rtree
*/
public void intersects(float x1, float y1, float x2, float y2,
IntProcedure intProcedure) {
Node rootNode = getNode(rootNodeId);
intersects(x1, y1, x2, y2, intProcedure, rootNode);
}
private void intersects(float x1, float y1, float x2, float y2,
IntProcedure v, Node n) {
for (int i = 0; i < n.entryCount; i++) {
if (intersects(x1, y1, x2, y2, n.entries[i])) {
if (n.isLeaf()) {
v.execute(n.ids[i]);
} else {
Node childNode = getNode(n.ids[i]);
intersects(x1, y1, x2, y2, v, childNode);
}
}
}
}
/**
* Determines if a line from start to end intersects a rectangle
*/
private boolean intersects(float x1, float y1, float x2, float y2,
Rectangle rectangle) {
double x = rectangle.min[0];
double y = rectangle.min[1];
if (x1 >= rectangle.min[0] && x1 <= rectangle.max[0]
&& y1 >= rectangle.min[1] && y1 <= rectangle.max[1])
return true;
if (x2 >= rectangle.min[0] && x2 <= rectangle.max[0]
&& y2 >= rectangle.min[1] && y2 <= rectangle.max[1])
return true;
double x3 = rectangle.max[0];
double y3 = rectangle.max[1];
return (Line2D.linesIntersect(x1, y1, x2, y2, x, y, x, y3)
|| Line2D.linesIntersect(x1, y1, x2, y2, x, y3, x3, y3)
|| Line2D.linesIntersect(x1, y1, x2, y2, x3, y3, x3, y) || Line2D
.linesIntersect(x1, y1, x2, y2, x3, y, x, y));
}
}