package spimedb.index.oct;
import org.eclipse.collections.impl.list.mutable.FastList;
import org.jetbrains.annotations.NotNull;
import spimedb.util.geom.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class OctBox<K> extends AABB implements Shape3D {
/**
* alternative tree recursion limit, number of world units when cells are
* not subdivided any further
* TODO resolution as array
*/
protected Vec3D resolution;
public final OctBox parent;
protected OctBox[] children;
protected Collection<IdBB> items;
/**
* Constructs a new AbstractOctree node within the AABB cube volume: {o.x, o.y,
* o.z} ... {o.x+size, o.y+size, o.z+size}
*
* @param o
* tree origin
* size of the tree volume along a single axis
*/
public OctBox(Vec3D o, Vec3D extents, Vec3D resolution) {
this(null, o, extents);
this.resolution = resolution;
if (resolution.volume() <= 0)
throw new RuntimeException("resolution must have non-zero volume"); //otherwise the root will hold everything due to root being always below threshold
}
/**
* Constructs a new AbstractOctree node within the AABB cube volume: {o.x, o.y,
* o.z} ... {o.x+size, o.y+size, o.z+size}
*
* @param p
* parent node
* @param o
* tree origin
* @param halfSize
* half length of the tree volume along a single axis
*/
private OctBox(OctBox p, Vec3D o, Vec3D halfSize) {
super(o.plus(halfSize), new Vec3D(halfSize));
this.parent = p;
if (parent != null) {
resolution = parent.resolution;
}
}
/** tests if the item is in this box (NOT recursively) */
public final boolean holds(IdBB item) {
return items.contains(item);
}
public final boolean holdsRecursively(IdBB item) {
if (!holds(item)) {
OctBox[] cc = this.children;
if (cc!=null) {
for (OctBox<K> c : cc) {
if (c != null && c.holdsRecursively(item))
return true;
}
}
}
return false;
}
public final int depth() {
final OctBox p = parent;
if (p == null) return 0;
return p.depth() + 1;
}
/**
* Adds all toAdd of the collection to the octree.
*
* @param toAdd
* point collection
* @return how many toAdd were added
*/
public final void putAll(final Iterable<? extends IdBB> toAdd) {
toAdd.forEach(this::put);
}
public final void forEachLocal(Consumer<IdBB> visitor) {
Collection<IdBB> ii = this.items;
if (ii !=null)
ii.forEach(visitor);
}
//TODO memoize this result in a special leaf subclass
public final boolean belowResolution(BB content) {
Vec3D e = extent;
Vec3D r = resolution;
XYZ ce = content.getExtents();
return e.x <= Math.max(r.x, ce.x()) ||
e.y <= Math.max(r.y, ce.y()) ||
e.z <= Math.max(r.z, ce.z());
}
/**
* Adds a new point/particle to the tree structure. All points are stored
* within leaf nodes only. The tree implementation is using lazy
* instantiation for all intermediate tree levels.
*
* @param p
* @return the box it was inserted to, or null if wasn't
*/
public OctBox<K> put(final IdBB x) {
BB p = x.getBB();
// check if point is inside cube
if (containsPoint(p)) {
//find the largest leaf that can contain x
if (belowResolution(x.getBB())) {
if (items == null) {
items = newItemCollection();
items.add(x);
onModified();
} else if (items.add(x)) {
onModified();
}
return this;
} else {
int octant = getOctantID(p);
boolean modified = false;
if (children == null) {
children = new OctBox[8];
modified = true;
}
final Vec3D extent = this.extent;
OctBox target = children[octant];
if (target == null) {
Vec3D off = new Vec3D(
minX() + ((octant & 1) != 0 ? extent.x() : 0),
minY() + ((octant & 2) != 0 ? extent.y() : 0),
minZ() + ((octant & 4) != 0 ? extent.z() : 0));
target = children[octant] = newBox(this, off, extent.scale(0.5f));
modified = true;
}
OctBox result = target.put(x);
if (result == null)
System.err.println(x + " was not inserted in a child of " + this);
//throw new NullPointerException
if (modified)
onModified();
return result;
}
}
return null;
}
@NotNull
protected OctBox newBox(OctBox parent, Vec3D off, Vec3D extent) {
return new OctBox(parent, off, extent);
}
//TODO pass the target box as a parameter so it can base its decision on that
protected Collection<IdBB> newItemCollection() {
//return new FastList();
return new HashSet<>();
}
public void forEachRecursive(Consumer<IdBB> visitor) {
forEachLocal(visitor);
OctBox[] cc = this.children;
if (cc !=null) {
for (OctBox<K> c : cc) {
if (c!=null)
c.forEachRecursive(visitor);
}
}
}
public void forEachRecursiveWithBox(BiConsumer<OctBox<K>, IdBB> visitor) {
forEachLocal(i -> {
visitor.accept(OctBox.this, i);
});
OctBox[] cc = this.children;
if (cc !=null) {
for (OctBox<K> c : cc) {
if (c!=null)
c.forEachRecursiveWithBox(visitor);
}
}
}
public void forEachBox(Consumer<OctBox<K>> visitor) {
visitor.accept(this);
OctBox[] cc = this.children;
if (cc !=null) {
for (OctBox c : cc) {
if (c != null) {
c.forEachBox(visitor);
}
}
}
}
public final boolean containsPoint(XYZ p) {
return p.isInAABB(this);
}
public void clear() {
zero();
children = null;
items = null;
}
/**
* @return a copy of the child nodes array
*/
public OctBox[] getChildrenCopy() {
if (children != null) {
return children.clone();
// OctBox[] clones = new OctBox[8];
// System.arraycopy(children, 0, clones, 0, 8);
// return clones;
}
return null;
}
/**
* Finds the leaf node which spatially relates to the given point
*
* @return leaf node or null if point is outside the tree dimensions
*/
public OctBox getLeafForPoint(final XYZ p) {
// if not a leaf node...
if (p.isInAABB(this)) {
final OctBox[] children = this.children;
if (children!=null) {
int octant = getOctantID(p);
if (children[octant] != null) {
return children[octant].getLeafForPoint(p);
}
} else if (items != null) {
return this;
}
}
return null;
}
/**
* Returns the minimum size of nodes (in world units). This value acts as
* tree recursion limit since nodes smaller than this size are not
* subdivided further. Leaf node are always smaller or equal to this size.
*
* @return the minimum size of tree nodes
*/
public final Vec3D getResolution() {
return resolution;
}
// /**
// * Computes the local child octant/cube index for the given point
// *
// * @param plocal
// * point in the node-local coordinate system
// * @return octant index
// */
// protected final int getOctantID(final Vec3D plocal) {
// final XYZ h = this.extent;
//
// return (plocal.x >= h.x() ? 1 : 0) + (plocal.y >= h.y() ? 2 : 0)
// + (plocal.z >= h.z() ? 4 : 0);
// }
/** computes getOctantID for the point subtracted by another point,
* without needing to allocate a temporary object
*/
private int getOctantID(final XYZ p) {
//final XYZ h = this.extent;
return ((p.x() - x) >= 0 ? 1 : 0) + ((p.y() - y) >= 0 ? 2 : 0)
+ ((p.z() - z) >= 0 ? 4 : 0);
}
/**
* @return the parent
*/
public final OctBox getParent() {
return parent;
}
public final Collection<IdBB> getItems() {
final Collection<IdBB> i = this.items;
if (i == null) return Collections.EMPTY_LIST;
return i;
}
public final int itemCountRecursively() {
final int[] x = {0};
forEachBox(n -> x[0] += n.itemCount());
return x[0];
}
public final int itemCount() {
final Collection<IdBB> i = this.items;
if (i == null) return 0;
return i.size();
}
public List<IdBB> getItemsRecursively() {
return getItemsRecursively(new FastList());
}
/**
* @return the points
*/
public List<IdBB> getItemsRecursively(List<IdBB> results) {
final OctBox[] children = this.children;
if (items != null) {
results.addAll(items);
} else if (children!=null) {
for (int i = 0; i < 8; i++) {
if (children[i] != null) {
children[i].getItemsRecursively(results);
}
}
}
return results;
}
/**
* Selects all stored points within the given axis-aligned bounding box.
*
* @param b
* AABB
* @return all points with the box volume
*/
@Deprecated public List<IdBB> getItemsWithin(BB b) {
List<IdBB> results = null;
if (this.intersectsBox(b)) {
if (items != null) {
for (IdBB q : items) {
if (q.getBB().isInAABB(b)) {
if (results == null) {
results = new FastList();
}
results.add(q);
}
}
} else if (children!=null) {
for (int i = 0; i < 8; i++) {
if (children[i] != null) {
List<IdBB> points = children[i].getItemsWithin(b);
if (points != null) {
if (results == null) {
results = new FastList();
}
results.addAll(points);
}
}
}
}
}
return results;
}
public void forEachBox(BB b, Consumer<IdBB> c) {
if (this.intersectsBox(b)) {
final OctBox[] childs = this.children;
if (items != null) {
for (IdBB q : items) {
if (q.getBB().isInAABB(b)) {
c.accept(q);
}
}
} else if (childs!=null) {
for (int i = 0; i < 8; i++) {
OctBox ci = childs[i];
if (ci != null) {
ci.forEachBox(b, c);
}
}
}
}
}
public void forEachNeighbor(IdBB item, XYZ boxRadius, Consumer<OctBox> visitor) {
//SOON
throw new UnsupportedOperationException();
}
public void forEachInSphere(Sphere s, Consumer<IdBB> c) {
if (this.intersectsSphere(s)) {
if (items != null) {
for (IdBB q : items) {
if (s.containsPoint(q.getBB())) {
c.accept(q);
}
}
} else if (children!=null) {
for (int i = 0; i < 8; i++) {
OctBox cc = children[i];
if (cc != null) {
cc.forEachInSphere(s, c);
}
}
}
}
}
/**
* Selects all stored points within the given sphere volume
*
* @param s
* sphere
* @return selected points
*/
@Deprecated public List<IdBB> getItemsWithin(Sphere s) {
List<IdBB> results = null;
if (this.intersectsSphere(s)) {
if (items != null) {
for (IdBB q : items) {
if (s.containsPoint(q.getBB())) {
if (results == null) {
results = new FastList();
}
results.add(q);
}
}
} else if (children!=null) {
for (int i = 0; i < 8; i++) {
if (children[i] != null) {
List<IdBB> points = children[i].getItemsWithin(s);
if (points != null) {
if (results == null) {
results = new FastList();
}
results.addAll(points);
}
}
}
}
}
return results;
}
/**
* Selects all stored points within the given sphere volume
*
* @param sphereOrigin
* @param clipRadius
* @return selected points
*/
public void forEachInSphere(Vec3D sphereOrigin, float clipRadius, Consumer<IdBB> c) {
forEachInSphere(new Sphere(sphereOrigin, clipRadius), c);
}
private boolean reduceBranch() {
boolean modified = false;
if (items != null && items.isEmpty()) {
items = null;
modified = true;
}
if (children!=null) {
int nullCount = 0;
for (int i = 0; i < 8; i++) {
OctBox ci = children[i];
if (ci != null) {
if ((ci.items == null)) {
children[i] = null;
modified = true;
nullCount++;
}
} else {
nullCount++;
}
}
if (nullCount == 8) {
children = null;
modified = true;
}
}
if (parent != null) {
if (parent.reduceBranch())
parent.onModified();
}
return modified;
}
protected void onModified() {
}
/**
* Removes a point from the tree and (optionally) tries to release memory by
* reducing now empty sub-branches.
*
* @return true, if the point was found & removed
*/
public boolean remove(Object _p) {
boolean found = false;
IdBB p = (IdBB)_p;
OctBox leaf = getLeafForPoint(p.getBB());
if (leaf != null) {
if (leaf.items.remove(p)) {
found = true;
if (leaf.items.size() == 0) {
leaf.reduceBranch();
}
}
}
if (found)
onModified();
return found;
}
public boolean removeAll(Collection<?> points) {
boolean allRemoved = true;
for (Object p : points) {
allRemoved &= remove(p);
}
return allRemoved;
}
/*
* (non-Javadoc)
*
* @see toxi.geom.AABB#toString()
*/
public String toString() {
Collection<IdBB> ii = this.items;
String x = "<OctBox:" + super.toString() + ":" +
((ii !=null) ? ii.size() : 0);
return x;
}
/** identified bounding box */
public static interface IdBB {
String id();
BB getBB();
}
}