/*
* Copyright (c) 2016 Fraunhofer IGD
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Fraunhofer IGD <http://www.igd.fraunhofer.de/>
*/
package de.fhg.igd.geom.indices;
import java.util.ArrayList;
import java.util.List;
import de.fhg.igd.geom.BoundingBox;
import de.fhg.igd.geom.Localizable;
/**
* Represents a node (or leaf) in a R-Tree. The splitting techniques used in
* this Node implements the Quadratic Algorithm described in Guttman - R-Trees:
* A dynamic index structure for spatial searching
*
* @author Michel Kraemer
* @param <T> the type of the objects stored in the RTree
*/
public class Node<T extends Localizable> implements Localizable {
/**
* The bounding box of this node
*/
private BoundingBox _boundingBox = new BoundingBox();
/**
* This node's children
*/
private List<Localizable> _children;
/**
* True, if this node is a leaf. All nodes are leafs until child nodes are
* added
*/
private boolean _isLeaf = true;
/**
* The node's parent or null if the node is the root
*/
private Node<T> _parent;
/**
* The page size (number of children/locs that can be attached to this node
* before it gets splitted). Must be even and greater than or equal to 4.
*/
private final int _pageSize;
/**
* The parent R-Tree
*/
private final RTree<T> _tree;
/**
* Default constructor
*
* @param pageSize the page size (number of children/locs that can be
* attached to this node before it gets splitted). Must be even
* and greater than or equal to 4.
* @param parent the node's parent or null if the node is the root
* @param tree the parent R-Tree
*/
public Node(int pageSize, Node<T> parent, RTree<T> tree) {
if (pageSize < 4) {
throw new IllegalArgumentException("pageSize must be " + "greater than or equal to 4");
}
if ((pageSize & 1) == 1) {
throw new IllegalArgumentException("pageSize must be even");
}
_pageSize = pageSize;
_parent = parent;
_tree = tree;
_children = new ArrayList<Localizable>(pageSize);
}
/**
* Calculates the minimum size of a Node
*
* @return the number of children a Node must have at least
*/
private int calculateMinSize() {
return _pageSize / 2;
}
/**
* In the RTree we can't use @link{BoundingBox#getVolume} as we want to
* index n-dimensional stuff. We make sure irregular stuff won't get in
* anyway, so be relaxed about BBs.
*
* @param bb the boundingBox
* @return the ersatz volume
*/
protected double calcPseudoVolume(BoundingBox bb) {
double nonDimensionSize = _tree.getNonDimensionSize();
double d = bb.getWidth();
double v = d > 0 ? d : nonDimensionSize;
d = bb.getHeight();
v *= d > 0 ? d : nonDimensionSize;
d = bb.getDepth();
v *= d > 0 ? d : nonDimensionSize;
return v;
}
/**
* In the RTree we can't use @link{BoundingBox#getVolume} as we want to
* index n-dimensional stuff. We make sure irregular stuff won't get in
* anyway, so be relaxed about BBs.
*
* @param bb the boundingBox
* @return the ersatz volume
*/
protected double calcPseudoVolume(Localizable bb) {
return calcPseudoVolume(bb.getBoundingBox());
}
/**
* Relates one bounding box to another
*
* @param b1 the first bounding box
* @param b2 the second bounding box
* @param useExtent true if the extents of the bounding boxes should be
* compared and not the bounding boxes themselves
* @return true if the first bounding box has any relation to the other one,
* false otherwise
*/
private static boolean relate(BoundingBox b1, BoundingBox b2, boolean useExtent) {
if (useExtent) {
return b1.toExtent().any(b2.toExtent());
}
return b1.any(b2);
}
/**
* Searches this Node and all children and returns the leaf that contains
* the given Localizable
*
* @param loc the Localizable to find
* @return the leaf that contains loc or null if loc is not contained by any
* leaf
*/
@SuppressWarnings("unchecked")
private Node<T> findLeaf(Localizable loc) {
if (!isLeaf()) {
// check all child nodes and invoke findLeaf() on them
// if they intersect with loc
for (Localizable l : _children) {
assert l instanceof Node;
if (relate(l.getBoundingBox(), loc.getBoundingBox(), false)) {
Node<T> n = ((Node<T>) l).findLeaf(loc);
if (n != null) {
return n;
}
}
}
}
else {
// check all children, if one of them matches
// loc return this leaf
for (Localizable l : _children) {
if (l == loc) {
return this;
}
}
}
return null;
}
/**
* Select a leaf node in which to place a new index entry.
*
* @param loc the localizable to find a leaf for
* @return the leaf node
*/
@SuppressWarnings({ "unchecked", "null" })
private Node<T> chooseLeaf(final Localizable loc) {
// if this is a leaf, return this
if (this.isLeaf()) {
return this;
}
// we're a node, so there must be children
assert _children != null;
// Guttman: find the smallest enlargement and find the
// bounding box with the smallest volume
double max = Double.POSITIVE_INFINITY;
Node<T> child = null;
for (Localizable l : _children) {
// l must be a Node, because this is no leaf (see above)
assert l instanceof Node;
Node<T> n = (Node<T>) l;
// calculate enlargement
BoundingBox larger = new BoundingBox(n.getBoundingBox());
larger.add(loc.getBoundingBox());
double nvolume = calcPseudoVolume(n);
double enlargement = calcPseudoVolume(larger) - nvolume;
if (enlargement < max) {
// use the one with the smallest enlargement
max = enlargement;
child = n;
}
else if (enlargement < max + 0.000001) { // roughly equal
// enlargement
// child cannot be null, because in the first loop it will
// always be set to n
assert child != null;
// use the one with the smallest volume
if (nvolume < calcPseudoVolume(child)) {
child = n;
}
}
}
// leaf cannot be null at this point,
// because there's always at least one child
assert child != null;
// descend
return child.chooseLeaf(loc);
}
/**
* Finds the first entries of two splitted groups (Quadratic Split)
*
* @return the two entries
*/
private Localizable[] pickSeeds() {
Localizable[] result = new Localizable[2];
int ii = -1, jj = -1;
double min = Double.NEGATIVE_INFINITY;
for (int i = 0; i < _children.size() - 1; ++i) {
for (int j = i + 1; j < _children.size(); ++j) {
Localizable e1 = _children.get(i);
Localizable e2 = _children.get(j);
// compose a new bounding box
BoundingBox bb = new BoundingBox(e1.getBoundingBox());
bb.add(e2.getBoundingBox());
// calculate waste
double d = calcPseudoVolume(bb) - calcPseudoVolume(e1) - calcPseudoVolume(e2);
if (d > min) {
result[0] = e1;
ii = i;
result[1] = e2;
jj = j;
min = d;
}
}
}
// result must not be empty
assert result[0] != null;
assert result[1] != null;
// remove entries from the list of Localizables
// make sure that we remove the correct element jj if
// ii has been removed before
_children.remove(ii);
_children.remove(ii < jj ? jj - 1 : jj);
return result;
}
@SuppressWarnings("unchecked")
private void plainAdd(Localizable loc) {
_children.add(loc);
_boundingBox.add(loc.getBoundingBox());
if (loc instanceof Node) {
((Node<T>) loc)._parent = this;
}
}
/**
* Splits this node into two nodes and adds them to the parent node.
* (Quadratic Split)
*
* @return the new second Node
*/
private Node<T> split() {
// choose two entries to be the first
// elements of the groups
Localizable[] seeds = pickSeeds();
// calculate minimum fill factor
int minSize = calculateMinSize();
// save this._children
List<Localizable> children = _children;
// assign each entry to a new Node
// this node will be group 0
Node<T> group0 = this;
_boundingBox = new BoundingBox();
_children = new ArrayList<Localizable>(_pageSize);
group0.plainAdd(seeds[0]);
Node<T> group1 = new Node<T>(_pageSize, this, _tree);
group1._isLeaf = this.isLeaf();
group1.plainAdd(seeds[1]);
// add next entries until nothing is left
while (children.size() > 0) {
// if one group has two few entries,
// add all remaining entries to it
if (minSize - group0._children.size() == children.size()) {
for (Localizable c : children) {
group0.plainAdd(c);
}
break;
}
else if (minSize - group1._children.size() == children.size()) {
for (Localizable c : children) {
group1.plainAdd(c);
}
break;
}
Localizable next = null;
double min = Double.NEGATIVE_INFINITY;
double dd0 = 0.0, dd1 = 0.0;
int ii = -1;
// iterate through children and get the one that causes
// the least cost
for (int i = 0; i < children.size(); ++i) {
Localizable l = children.get(i);
BoundingBox eb = l.getBoundingBox();
// calculate how much group 0 had to be extended if
// we would add the child to it
BoundingBox bb = new BoundingBox(group0.getBoundingBox());
bb.add(eb);
double d0 = calcPseudoVolume(bb) - calcPseudoVolume(group0);
// calculate the same for group 2
bb = new BoundingBox(group1.getBoundingBox());
bb.add(eb);
double d1 = calcPseudoVolume(bb) - calcPseudoVolume(group1);
// calculate the least cost
double diff = Math.abs(d1 - d0);
if (diff > min) {
next = l;
ii = i;
min = diff;
// remember the cost
dd0 = d0;
dd1 = d1;
}
}
// we must have a next entity now
assert next != null;
// remove entry from the list of Localizables
children.remove(ii);
// add result to one of the groups
if (dd0 < dd1) {
group0.plainAdd(next);
}
else if (dd1 < dd0) {
group1.plainAdd(next);
}
else {
double a0 = calcPseudoVolume(group0);
double a1 = calcPseudoVolume(group1);
if (a0 < a1) {
group0.plainAdd(next);
}
else if (a1 < a0) {
group1.plainAdd(next);
}
else {
if (group1._children.size() < group0._children.size()) {
group1.plainAdd(next);
}
else {
group0.plainAdd(next);
}
}
}
}
return group1;
}
/**
* Inserts a Localizable to the Node. Splits the Node if there are too many
* children.
*
* @param loc the Localizable to insert
*/
private void internalInsert(final Localizable loc) {
if (!isLeaf() && !(loc instanceof Node<?>)) {
throw new IllegalArgumentException("You may not add a " + "Localizable to a Node");
}
// check if this is a leaf or a node
if (_children.size() == 0 && (loc instanceof Node<?>)) {
_isLeaf = false;
}
// if there is enough space, add loc
if (_children.size() < _pageSize) {
plainAdd(loc);
// propagate changes upward
adjustTree(null);
}
else {
plainAdd(loc);
Node<T> second = split();
// propagate changes upward
adjustTree(second);
// add group 1 to parent
if (_parent == null) {
// node is obviously the root
// create a new root
Node<T> newroot = new Node<T>(_pageSize, null, _tree);
// set new root
_tree.setRoot(newroot);
// insert child elements
newroot.internalInsert(this);
newroot.internalInsert(second);
// the following line is not needed, because
// the insert method sets the parent
// _parent = newroot;
}
}
}
/**
* Finds the Leaf L in which to insert the given Localizable loc and inserts
* loc to L. Splits L if there are too many children.
*
* @param loc the Localizable to insert
*/
public void insert(final Localizable loc) {
// find the leaf in which to place loc
Node<T> leaf = chooseLeaf(loc);
leaf.internalInsert(loc);
}
/**
* Adds the BoundingBox of this Node to all parent nodes and adds the
* Localizable nn, which resulted from a split operation, to the parent.
*
* @param nn the Localizable produced by a split operation
*/
private void adjustTree(Node<T> nn) {
if (_parent == null) {
return;
}
_parent._boundingBox.add(_boundingBox);
if (nn != null) {
_parent.internalInsert(nn);
}
else {
_parent.adjustTree(null);
}
}
/**
* Traverses the given Node "no" and all its children. If a leaf is found
* its children (the actual Localizables) will be inserted into the given
* RTree as usual.
*
* @param <T> the type of the objects stored in the RTree
* @param no the Node to traverse
* @param tree the RTree to insert the Localizables into
*/
@SuppressWarnings("unchecked")
private static <T extends Localizable> void insertChildrenOfAllLeafs(Node<T> no,
RTree<T> tree) {
if (no.isLeaf()) {
// re-insert the leaf's children as usual
for (Localizable c : no.getChildren()) {
tree.insert((T) c);
}
}
else {
// no is no leaf, so descend...
for (Localizable c : no.getChildren()) {
assert c instanceof Node;
insertChildrenOfAllLeafs((Node<T>) c, tree);
}
}
}
/**
* Eliminate l if it has too few entries. Propagate elimination upwards.
* Adjust BoundingBoxes.
*
* @param l the Leaf from which an entry has been deleted
*/
@SuppressWarnings("unchecked")
private void condenseTree(Node<T> l) {
// the list of deleted nodes
List<Node<T>> q = new ArrayList<Node<T>>();
Node<T> n = l;
while (n._parent != null) {
// eliminate under-full node
int minSize = calculateMinSize();
if (n._children.size() < minSize) {
n._parent._children.remove(n);
q.add(n);
}
else {
// adjust BoundingBox of n (the element from which an
// entry has been deleted)
n._boundingBox = new BoundingBox();
for (Localizable c : n._children) {
n._boundingBox.add(c.getBoundingBox());
}
}
n = n._parent;
}
// adjust the bounding box of the root
n._boundingBox = new BoundingBox();
for (Localizable c : n._children) {
n._boundingBox.add(c.getBoundingBox());
}
// re-insert orphaned entries
for (Node<T> no : q) {
if (no.isLeaf()) {
// re-insert the leaf's children as usual
for (Localizable c : no.getChildren()) {
_tree.insert((T) c);
}
}
else {
// insert the children of all leafs we can
// find in "no"
insertChildrenOfAllLeafs(no, _tree);
}
}
}
/**
* Removes a Localizable from the Node
*
* @param loc the Localizable to remove
* @return true if the Node has been changed, false otherwise
*/
@SuppressWarnings("unchecked")
public boolean delete(Localizable loc) {
// find the leaf containing loc
Node<T> l = findLeaf(loc);
if (l == null) {
return false;
}
// remove loc from the leaf
l._children.remove(loc);
// propagate changes
condenseTree(l);
// shorten tree (if the root has only one child element,
// make this element the new root)
if (_tree.getRoot()._children.size() == 1 && !_tree.getRoot().isLeaf()) {
_tree.setRoot((Node<T>) _tree.getRoot()._children.get(0));
_tree.getRoot()._parent = null;
}
return true;
}
/**
* Returns a list of all Localizables that have any relation to the given
* Localizable loc.
*
* @param loc the Localizable to match
* @param ignoreZ true if the z coordinate should be ignored during
* candidate search
* @return a list of Localizables (containing only leafs of this tree and no
* nodes)
*/
@SuppressWarnings("unchecked")
private List<T> find(final Localizable loc, boolean ignoreZ) {
List<T> result = new ArrayList<T>();
BoundingBox theirs = loc.getBoundingBox();
for (Localizable l : _children) {
BoundingBox ours = l.getBoundingBox();
if (relate(ours, theirs, ignoreZ)) {
if (l instanceof Node) {
// descend
result.addAll(((Node<T>) l).find(loc, ignoreZ));
}
else {
// add leaf
result.add((T) l);
}
}
}
return result;
}
/**
* Returns a list of all Localizables that have any relation to the given
* Localizable loc.
*
* @param loc the Localizable to match
* @return a list of Localizables (containing only leafs of this tree and no
* nodes)
*/
public List<T> find(final Localizable loc) {
return find(loc, false);
}
/**
* Returns a list of all Localizables that have any relation to the given
* Localizable loc. Ignores the z ordinate.
*
* @param loc the Localizable to match
* @return a list of Localizables (containing only leafs of this tree and no
* nodes)
*/
public List<T> find2D(final Localizable loc) {
return find(loc, true);
}
/**
* This method will return the "Neighborhood" of a given Localizable.
*
* @param k the number of neighbor candidates to retrieve
* @param loc the given Localizable
* @param stepsize the size of each step in which the neighborhood is
* enlarged. If the original Localizable had no volume (because
* it's a point type), this value is first used as an absolute
* value and then as a successive relative increase
* @return a list with all neighbors
*/
public ArrayList<T> findNeighborhood(int k, Localizable loc, double stepsize) {
ArrayList<T> result = new ArrayList<T>();
result.addAll(this.find(loc));
BoundingBox bb = loc.getBoundingBox();
while (result.size() < k && this.getBoundingBox().intersectsOrCovers(bb)) {
bb.expand(stepsize);
result.addAll(this.find(bb));
}
return result;
}
/**
* @see Localizable#getBoundingBox()
*/
@Override
public BoundingBox getBoundingBox() {
return _boundingBox;
}
/**
* @return true if this node is a leaf (has no children), false otherwise
*/
public boolean isLeaf() {
return _isLeaf;
}
/**
* @return this node's children or null if this node is a leaf
*/
public List<Localizable> getChildren() {
return _children;
}
}