org.spf4j.ds.RTree Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spf4j-core Show documentation
Show all versions of spf4j-core Show documentation
A continuously growing collection of utilities to measure performance, get better diagnostics,
improve performance, or do things more reliably, faster that other open source libraries...
/*
* Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* Additionally licensed with:
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.spf4j.ds;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Implementation of an arbitrary-dimension RTree. Based on R-Trees: A Dynamic Index Structure for Spatial Searching
* (Antonn Guttmann, 1984)
*
* This class is not thread-safe. Z TODO: I have cleaned up a bit this class, but there is a lot more to do here this
* class implementation is not clean in several places. (see findbugs supressions)
*
* @param the type of entry to store in this RTree.
*/
@SuppressFBWarnings({"CLI_CONSTANT_LIST_INDEX", "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE",
"PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS", "PREDICTABLE_RANDOM", "IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER"})
public final class RTree {
private static final float DIM_FACTOR = -2.0f;
private static final float FUDGE_FACTOR = 1.001f;
private static final float INIT_COORD = (float) Math.sqrt(Float.MAX_VALUE);
public enum SeedPicker {
LINEAR, QUADRATIC
}
private final int maxEntries;
private final int minEntries;
private final int numDims;
private final float[] pointDims;
private final SeedPicker seedPicker;
private Node root;
private int size;
/**
* Creates a new RTree.
*
* @param maxEntries maximum number of entries per node
* @param minEntries minimum number of entries per node (except for the root node)
* @param numDims the number of dimensions of the RTree.
*/
public RTree(final int maxEntries, final int minEntries, final int numDims,
final SeedPicker seedPicker) {
assert (minEntries <= (maxEntries / 2));
this.numDims = numDims;
this.maxEntries = maxEntries;
this.minEntries = minEntries;
this.seedPicker = seedPicker;
pointDims = new float[numDims];
root = buildRoot(true);
}
public RTree(final int maxEntries, final int minEntries, final int numDims) {
this(maxEntries, minEntries, numDims, SeedPicker.LINEAR);
}
private Node buildRoot(final boolean asLeaf) {
float[] initCoords = new float[numDims];
float[] initDimensions = new float[numDims];
for (int i = 0; i < this.numDims; i++) {
initCoords[i] = INIT_COORD;
initDimensions[i] = DIM_FACTOR * INIT_COORD;
}
return new Node(initCoords, initDimensions, asLeaf);
}
/**
* Builds a new RTree using default parameters: maximum 50 entries per node minimum 2 entries per node 2 dimensions
*/
public RTree() {
this(50, 2, 2, SeedPicker.LINEAR);
}
public RTree(final int numDimensions) {
this(50, 2, numDimensions, SeedPicker.LINEAR);
}
/**
* @return the maximum number of entries per node
*/
public int getMaxEntries() {
return maxEntries;
}
/**
* @return the minimum number of entries per node for all nodes except the root.
*/
public int getMinEntries() {
return minEntries;
}
/**
* @return the number of dimensions of the tree
*/
public int getNumDims() {
return numDims;
}
/**
* @return the number of items in this tree.
*/
public int size() {
return size;
}
/**
* Searches the RTree for objects overlapping with the given rectangle.
*
* @param coords the corner of the rectangle that is the lower bound of every dimension (eg. the top-left corner)
* @param dimensions the dimensions of the rectangle.
* @return a list of objects whose rectangles overlap with the given rectangle.
*/
@Nonnull
public List search(final float[] coords, final float[] dimensions) {
assert (coords.length == numDims);
assert (dimensions.length == numDims);
LinkedList results = new LinkedList();
search(coords, dimensions, root, results);
return results;
}
private void search(final float[] coords, final float[] dimensions, final Node n,
final LinkedList results) {
if (n.leaf) {
for (Node e : n.children) {
if (isOverlap(coords, dimensions, e.coords, e.dimensions)) {
results.add(((Entry) e).entry);
}
}
} else {
for (Node c : n.children) {
if (isOverlap(coords, dimensions, c.coords, c.dimensions)) {
search(coords, dimensions, c, results);
}
}
}
}
/**
* Deletes the entry associated with the given rectangle from the RTree
*
* @param coords the corner of the rectangle that is the lower bound in every dimension
* @param dimensions the dimensions of the rectangle
* @param entry the entry to delete
* @return true iff the entry was deleted from the RTree.
*/
public boolean delete(final float[] coords, final float[] dimensions, final T entry) {
assert (coords.length == numDims);
assert (dimensions.length == numDims);
Node l = findLeaf(root, coords, dimensions, entry);
if (l == null) {
throw new IllegalArgumentException("leaf not found for entry " + entry);
}
ListIterator li = l.children.listIterator();
T removed = null;
while (li.hasNext()) {
@SuppressWarnings("unchecked")
Entry e = (Entry) li.next();
if (e.entry.equals(entry)) {
removed = e.entry;
li.remove();
break;
}
}
if (removed != null) {
condenseTree(l);
size--;
}
if (size == 0) {
root = buildRoot(true);
}
return (removed != null);
}
public boolean delete(final float[] coords, final T entry) {
return delete(coords, pointDims, entry);
}
@Nullable
private Node findLeaf(final Node n, final float[] coords,
final float[] dimensions, final T entry) {
if (n.leaf) {
for (Node c : n.children) {
if (((Entry) c).entry.equals(entry)) {
return n;
}
}
return null;
} else {
for (Node c : n.children) {
if (isOverlap(c.coords, c.dimensions, coords, dimensions)) {
Node result = findLeaf(c, coords, dimensions, entry);
if (result != null) {
return result;
}
}
}
return null;
}
}
private void condenseTree(final Node pn) {
Node n = pn;
Set q = new HashSet();
while (n != root) {
if (n.leaf && (n.children.size() < minEntries)) {
q.addAll(n.children);
n.parent.children.remove(n);
} else if (!n.leaf && (n.children.size() < minEntries)) {
// probably a more efficient way to do this...
LinkedList toVisit = new LinkedList(n.children);
while (!toVisit.isEmpty()) {
Node c = toVisit.pop();
if (c.leaf) {
q.addAll(c.children);
} else {
toVisit.addAll(c.children);
}
}
n.parent.children.remove(n);
} else {
tighten(n);
}
n = n.parent;
}
if (root.children.size() == 0) {
root = buildRoot(true);
} else if ((!root.leaf) && (root.children.size() == 1)) {
root = root.children.get(0);
root.parent = null;
} else {
tighten(root);
}
for (Node ne : q) {
@SuppressWarnings("unchecked")
Entry e = (Entry) ne;
insert(e.coords, e.dimensions, e.entry);
}
size -= q.size();
}
/**
* Empties the RTree
*/
public void clear() {
root = buildRoot(true);
// let the GC take care of the rest.
}
/**
* Inserts the given entry into the RTree, associated with the given rectangle.
*
* @param coords the corner of the rectangle that is the lower bound in every dimension
* @param dimensions the dimensions of the rectangle
* @param entry the entry to insert
*/
public void insert(final float[] coords, final float[] dimensions, final T entry) {
assert (coords.length == numDims);
assert (dimensions.length == numDims);
Entry e = new Entry(coords, dimensions, entry);
Node l = chooseLeaf(root, e);
l.children.add(e);
size++;
e.parent = l;
if (l.children.size() > maxEntries) {
Node[] splits = splitNode(l);
adjustTree(splits[0], splits[1]);
} else {
adjustTree(l, null);
}
}
/**
* Convenience method for inserting a point
*
* @param coords
* @param entry
*/
public void insert(final float[] coords, final T entry) {
insert(coords, pointDims, entry);
}
private void adjustTree(final Node n, final Node nn) {
if (n == root) {
if (nn != null) {
// build new root and add children.
root = buildRoot(false);
root.children.add(n);
n.parent = root;
root.children.add(nn);
nn.parent = root;
}
tighten(root);
return;
}
tighten(n);
if (nn != null) {
tighten(nn);
if (n.parent.children.size() > maxEntries) {
Node[] splits = splitNode(n.parent);
adjustTree(splits[0], splits[1]);
}
}
if (n.parent != null) {
adjustTree(n.parent, null);
}
}
private Node[] splitNode(final Node n) {
// TODO: this class probably calls "tighten" a little too often.
// For instance the call at the end of the "while (!cc.isEmpty())" loop
// could be modified and inlined because it's only adjusting for the addition
// of a single node. Left as-is for now for readability.
@SuppressWarnings("unchecked")
Node[] nn = new RTree.Node[]{n, new Node(n.coords, n.dimensions, n.leaf)};
final Node nn1 = nn[1];
nn1.parent = n.parent;
if (nn1.parent != null) {
nn1.parent.children.add(nn1);
}
LinkedList cc = new LinkedList(n.children);
n.children.clear();
Node[] ss = seedPicker == SeedPicker.LINEAR ? lPickSeeds(cc) : qPickSeeds(cc);
final Node nn0 = nn[0];
nn0.children.add(ss[0]);
nn1.children.add(ss[1]);
tighten(nn);
while (!cc.isEmpty()) {
final int size0 = nn0.children.size();
final int size1 = nn1.children.size();
if ((size0 >= minEntries)
&& (size1 + cc.size() == minEntries)) {
nn1.children.addAll(cc);
cc.clear();
tighten(nn); // Not sure this is required.
return nn;
} else if ((size1 >= minEntries)
&& (size0 + cc.size() == minEntries)) {
nn0.children.addAll(cc);
cc.clear();
tighten(nn); // Not sure this is required.
return nn;
}
Node c = seedPicker == SeedPicker.LINEAR ? lPickNext(cc) : qPickNext(cc, nn);
Node preferred;
float e0 = getRequiredExpansion(nn0.coords, nn0.dimensions, c);
float e1 = getRequiredExpansion(nn1.coords, nn1.dimensions, c);
if (e0 < e1) {
preferred = nn0;
} else if (e0 > e1) {
preferred = nn1;
} else {
float a0 = getArea(nn0.dimensions);
float a1 = getArea(nn1.dimensions);
if (a0 < a1) {
preferred = nn0;
} else if (e0 > a1) {
preferred = nn1;
} else {
if (size0 < size1) {
preferred = nn0;
} else if (size0 > size1) {
preferred = nn1;
} else {
preferred = nn[(int) Math.round(Math.random())];
}
}
}
preferred.children.add(c);
tighten(preferred);
}
return nn;
}
// Implementation of Quadratic PickSeeds
@SuppressFBWarnings("MRC_METHOD_RETURNS_CONSTANT")
private Node[] qPickSeeds(final List nn) {
@SuppressWarnings("unchecked")
Node[] bestPair = new Node[2];
float maxWaste = -1.0f * Float.MAX_VALUE;
for (Node n1 : nn) {
for (Node n2 : nn) {
if (n1 == n2) {
continue;
}
float n1a = getArea(n1.dimensions);
float n2a = getArea(n2.dimensions);
float ja = 1.0f;
for (int i = 0; i < numDims; i++) {
final float n1coordsi = n1.coords[i];
final float n2coordsi = n2.coords[i];
float jc0 = Math.min(n1coordsi, n2coordsi);
float jc1 = Math.max(n1coordsi + n1.dimensions[i], n2coordsi + n2.dimensions[i]);
ja *= (jc1 - jc0);
}
float waste = ja - n1a - n2a;
if (waste > maxWaste) {
maxWaste = waste;
bestPair[0] = n1;
bestPair[1] = n2;
}
}
}
nn.remove(bestPair[0]);
nn.remove(bestPair[1]);
return bestPair;
}
/**
* Implementation of QuadraticPickNext
*
* @param cc the children to be divided between the new nodes, one item will be removed from this list.
* @param nn the candidate nodes for the children to be added to.
*/
private Node qPickNext(final LinkedList cc, final Node[] nn) {
float maxDiff = -1.0f * Float.MAX_VALUE;
Node nextC = null;
for (Node c : cc) {
float n0Exp = getRequiredExpansion(nn[0].coords, nn[0].dimensions, c);
float n1Exp = getRequiredExpansion(nn[1].coords, nn[1].dimensions, c);
float diff = Math.abs(n1Exp - n0Exp);
if (diff > maxDiff) {
maxDiff = diff;
nextC = c;
}
}
assert (nextC != null) : "No node selected from qPickNext";
cc.remove(nextC);
return nextC;
}
// Implementation of LinearPickSeeds
private Node[] lPickSeeds(final LinkedList nn) {
@SuppressWarnings("unchecked")
Node[] bestPair = new RTree.Node[2];
boolean foundBestPair = false;
float bestSep = 0.0f;
for (int i = 0; i < numDims; i++) {
float dimLb = Float.MAX_VALUE, dimMinUb = Float.MAX_VALUE;
float dimUb = -1.0f * Float.MAX_VALUE, dimMaxLb = -1.0f * Float.MAX_VALUE;
Node nMaxLb = null, nMinUb = null;
for (Node n : nn) {
if (n.coords[i] < dimLb) {
dimLb = n.coords[i];
}
if (n.dimensions[i] + n.coords[i] > dimUb) {
dimUb = n.dimensions[i] + n.coords[i];
}
if (n.coords[i] > dimMaxLb) {
dimMaxLb = n.coords[i];
nMaxLb = n;
}
if (n.dimensions[i] + n.coords[i] < dimMinUb) {
dimMinUb = n.dimensions[i] + n.coords[i];
nMinUb = n;
}
}
float sep = (nMaxLb == nMinUb) ? -1.0f
: Math.abs((dimMinUb - dimMaxLb) / (dimUb - dimLb));
if (sep >= bestSep) {
bestPair[0] = nMaxLb;
bestPair[1] = nMinUb;
bestSep = sep;
foundBestPair = true;
}
}
// In the degenerate case where all points are the same, the above
// algorithm does not find a best pair. Just pick the first 2
// children.
if (!foundBestPair) {
bestPair = new RTree.Node[]{nn.get(0), nn.get(1)};
}
nn.remove(bestPair[0]);
nn.remove(bestPair[1]);
return bestPair;
}
/**
* Implementation of LinearPickNext
*
* @param cc the children to be divided between the new nodes, one item will be removed from this list.
*/
private Node lPickNext(final Deque cc) {
return cc.pop();
}
private void tighten(final Node... nodes) {
assert (nodes.length >= 1) : "Pass some nodes to tighten!";
for (Node n : nodes) {
assert (n.children.size() > 0) : "tighten() called on empty node!";
float[] minCoords = new float[numDims];
float[] maxCoords = new float[numDims];
for (int i = 0; i < numDims; i++) {
minCoords[i] = Float.MAX_VALUE;
maxCoords[i] = Float.MIN_VALUE;
for (Node c : n.children) {
// we may have bulk-added a bunch of children to a node (eg. in
// splitNode)
// so here we just enforce the child->parent relationship.
c.parent = n;
if (c.coords[i] < minCoords[i]) {
minCoords[i] = c.coords[i];
}
if ((c.coords[i] + c.dimensions[i]) > maxCoords[i]) {
maxCoords[i] = (c.coords[i] + c.dimensions[i]);
}
}
}
for (int i = 0; i < numDims; i++) {
// Convert max coords to dimensions
maxCoords[i] -= minCoords[i];
}
System.arraycopy(minCoords, 0, n.coords, 0, numDims);
System.arraycopy(maxCoords, 0, n.dimensions, 0, numDims);
}
}
private Node chooseLeaf(final Node n, final Entry e) {
if (n.leaf) {
return n;
}
float minInc = Float.MAX_VALUE;
Node next = null;
for (Node c : n.children) {
float inc = getRequiredExpansion(c.coords, c.dimensions, e);
if (inc < minInc) {
minInc = inc;
next = c;
} else if (inc == minInc) {
float curArea = 1.0f;
float thisArea = 1.0f;
for (int i = 0; i < c.dimensions.length; i++) {
if (next == null) {
throw new IllegalStateException("Illegal state at " + c);
}
curArea *= next.dimensions[i];
thisArea *= c.dimensions[i];
}
if (thisArea < curArea) {
next = c;
}
}
}
return chooseLeaf(next, e);
}
/**
* Returns the increase in area necessary for the given rectangle to cover the given entry.
*/
private float getRequiredExpansion(final float[] coords, final float[] dimensions, final Node e) {
float area = getArea(dimensions);
float[] deltas = new float[dimensions.length];
for (int i = 0; i < deltas.length; i++) {
if (coords[i] + dimensions[i] < e.coords[i] + e.dimensions[i]) {
deltas[i] = e.coords[i] + e.dimensions[i] - coords[i] - dimensions[i];
} else if (coords[i] + dimensions[i] > e.coords[i] + e.dimensions[i]) {
deltas[i] = coords[i] - e.coords[i];
}
}
float expanded = 1.0f;
for (int i = 0; i < dimensions.length; i++) {
area *= dimensions[i] + deltas[i];
}
return (expanded - area);
}
private float getArea(final float[] dimensions) {
float area = 1.0f;
for (int i = 0; i < dimensions.length; i++) {
area *= dimensions[i];
}
return area;
}
private boolean isOverlap(final float[] scoords, final float[] sdimensions,
final float[] coords, final float[] dimensions) {
for (int i = 0; i < scoords.length; i++) {
boolean overlapInThisDimension = false;
if (scoords[i] == coords[i]) {
overlapInThisDimension = true;
} else if (scoords[i] < coords[i]) {
if (scoords[i] + FUDGE_FACTOR * sdimensions[i] >= coords[i]) {
overlapInThisDimension = true;
}
} else if (scoords[i] > coords[i]) {
if (coords[i] + FUDGE_FACTOR * dimensions[i] >= scoords[i]) {
overlapInThisDimension = true;
}
}
if (!overlapInThisDimension) {
return false;
}
}
return true;
}
// CHECKSTYLE:OFF
private static class Node {
final float[] coords;
final float[] dimensions;
final LinkedList children;
final boolean leaf;
Node parent;
private Node(float[] coords, float[] dimensions, boolean leaf) {
this.coords = new float[coords.length];
this.dimensions = new float[dimensions.length];
System.arraycopy(coords, 0, this.coords, 0, coords.length);
System.arraycopy(dimensions, 0, this.dimensions, 0, dimensions.length);
this.leaf = leaf;
children = new LinkedList<>();
}
@Override
public String toString() {
return "Node{" + "coords=" + Arrays.toString(coords) + ", dimensions="
+ Arrays.toString(dimensions) + ", children="
+ children + ", leaf=" + leaf + ", parent=" + parent + '}';
}
}
private static class Entry extends Node {
public final T entry;
public Entry(final float[] coords, final float[] dimensions, final T entry) {
// an entry isn't actually a leaf (its parent is a leaf)
// but all the algorithms should stop at the first leaf they encounter,
// so this little hack shouldn't be a problem.
super(coords, dimensions, true);
this.entry = entry;
}
@Override
public String toString() {
return "Entry: " + entry;
}
}
//CHECKSTYLE:ON
// The methods below this point can be used to create an HTML rendering
// of the RTree. Maybe useful for debugging?
private static final int ELEM_WIDTH = 150;
private static final int ELEM_HEIGHT = 120;
public String visualize() {
int ubDepth = (int) Math.ceil(Math.log(size) / Math.log(minEntries)) * ELEM_HEIGHT;
int ubWidth = size * ELEM_WIDTH;
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
pw.println("");
visualize(root, pw, 0, 0, ubWidth, ubDepth);
pw.println("");
pw.flush();
return sw.toString();
}
private void visualize(final Node n, final java.io.PrintWriter pw, final int x0,
final int y0, final int w, final int h) {
pw.printf("%n",
x0, y0, w, h);
pw.println("");
pw.println("Node: " + n + " (root==" + (n == root) + ") \n");
pw.println("Coords: " + Arrays.toString(n.coords) + '\n');
pw.println("Dimensions: " + Arrays.toString(n.dimensions) + '\n');
pw.println("# Children: " + ((n.children == null) ? 0 : n.children.size()) + '\n');
pw.println("isLeaf: " + n.leaf + '\n');
pw.println("
");
int numChildren = (n.children == null) ? 0 : n.children.size();
for (int i = 0; i < numChildren; i++) {
visualize(n.children.get(i), pw, (int) (x0 + (i * w / (float) numChildren)),
y0 + ELEM_HEIGHT, (int) (w / (float) numChildren), h - ELEM_HEIGHT);
}
pw.println("");
}
@Override
public String toString() {
return "RTree{" + "maxEntries=" + maxEntries + ", minEntries=" + minEntries + ", numDims="
+ numDims + ", pointDims=" + Arrays.toString(pointDims)
+ ", seedPicker=" + seedPicker + ", root=" + root + ", size=" + size + '}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy