Add the ability to limit how deep a quad tree can go

This commit is contained in:
James Seibel
2023-03-23 21:04:27 -05:00
parent d4b6ec74a8
commit 8a32d7f84a
3 changed files with 197 additions and 40 deletions
@@ -5,6 +5,7 @@ import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class QuadNode<T>
@@ -12,7 +13,8 @@ public class QuadNode<T>
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public DhSectionPos sectionPos;
public final DhSectionPos sectionPos;
public final byte minimumDetailLevel;
public T value;
@@ -45,9 +47,10 @@ public class QuadNode<T>
public QuadNode(DhSectionPos sectionPos)
public QuadNode(DhSectionPos sectionPos, byte minimumDetailLevel)
{
this.sectionPos = sectionPos;
this.minimumDetailLevel = minimumDetailLevel;
}
@@ -116,6 +119,8 @@ public class QuadNode<T>
*/
private T getOrSetValue(DhSectionPos inputSectionPos, boolean replaceValue, T newValue) throws IllegalArgumentException
{
// debug validation
if (!this.sectionPos.contains(inputSectionPos))
{
LOGGER.error((replaceValue ? "set " : "get ")+inputSectionPos+" center block: "+inputSectionPos.getCenter().getCornerBlockPos()+", this pos: "+this.sectionPos+" this center block: "+this.sectionPos.getCenter().getCornerBlockPos());
@@ -132,6 +137,14 @@ public class QuadNode<T>
throw new IllegalArgumentException("Node and input detail level are equal, however positions are not; this tree doesn't contain the requested position. Node pos: "+this.sectionPos+", input pos: "+inputSectionPos);
}
if (inputSectionPos.sectionDetailLevel < this.minimumDetailLevel)
{
throw new IllegalArgumentException("Input position is requesting a detail level lower than what this node can provide. Node minimum detail level: "+this.minimumDetailLevel+", input pos: "+inputSectionPos);
}
// get/set logic
if (inputSectionPos.sectionDetailLevel == this.sectionPos.sectionDetailLevel)
{
// this node is the requested position
@@ -162,7 +175,7 @@ public class QuadNode<T>
if (replaceValue && this.nwChild == null)
{
// if no node exists for this position, but we want to insert a new value at this position, create a new node
this.nwChild = new QuadNode<>(nwPos);
this.nwChild = new QuadNode<>(nwPos, this.minimumDetailLevel);
}
childNode = this.nwChild;
@@ -175,7 +188,7 @@ public class QuadNode<T>
if (replaceValue && this.swChild == null)
{
// if no node exists for this position, but we want to insert a new value at this position, create a new node
this.swChild = new QuadNode<>(swPos);
this.swChild = new QuadNode<>(swPos, this.minimumDetailLevel);
}
childNode = this.swChild;
@@ -188,7 +201,7 @@ public class QuadNode<T>
if (replaceValue && this.neChild == null)
{
// if no node exists for this position, but we want to insert a new value at this position, create a new node
this.neChild = new QuadNode<>(nePos);
this.neChild = new QuadNode<>(nePos, this.minimumDetailLevel);
}
childNode = this.neChild;
@@ -201,7 +214,7 @@ public class QuadNode<T>
if (replaceValue && this.seChild == null)
{
// if no node exists for this position, but we want to insert a new value at this position, create a new node
this.seChild = new QuadNode<>(sePos);
this.seChild = new QuadNode<>(sePos, this.minimumDetailLevel);
}
childNode = this.seChild;
@@ -221,11 +234,15 @@ public class QuadNode<T>
* Applies the given consumer to all 4 of this nodes' children. <br>
* Note: this will pass in null children.
*/
public void forEachDirectChild(Consumer<QuadNode<T>> callback)
public void forEachDirectChild(BiConsumer<QuadNode<T>, DhSectionPos> callback)
{
for (int i = 0; i < 4; i++)
if (this.sectionPos.sectionDetailLevel != this.minimumDetailLevel)
{
callback.accept(this.getChildByIndex(i));
for (int i = 0; i < 4; i++)
{
DhSectionPos childPos = this.sectionPos.getChildByIndex(i);
callback.accept(this.getChildByIndex(i), childPos);
}
}
}
@@ -235,7 +252,7 @@ public class QuadNode<T>
*/
public void forAllLeafValues(Consumer<? super T> callback)
{
if (this.childCount() == 0)
if (this.childCount() == 0 || this.sectionPos.sectionDetailLevel == this.minimumDetailLevel)
{
// base case, bottom leaf node found
callback.accept(this.value);
@@ -254,6 +271,45 @@ public class QuadNode<T>
}
}
public void deleteAllChildren() { this.deleteAllChildren(null); }
/** @param removedItemConsumer is only fired for non-null nodes, however the value passed in may be null */
public void deleteAllChildren(Consumer<? super T> removedItemConsumer)
{
for (int i = 0; i < 4; i++)
{
QuadNode<T> childNode = this.getChildByIndex(i);
if (childNode != null)
{
childNode.deleteAllChildren(removedItemConsumer);
}
}
if (nwChild != null)
{
removedItemConsumer.accept(nwChild.value);
}
nwChild = null;
if (neChild != null)
{
removedItemConsumer.accept(neChild.value);
}
neChild = null;
if (seChild != null)
{
removedItemConsumer.accept(seChild.value);
}
seChild = null;
if (swChild != null)
{
removedItemConsumer.accept(swChild.value);
}
swChild = null;
}
@Override
public String toString() { return "pos: "+this.sectionPos+", value: "+this.value; }
@@ -20,14 +20,14 @@ import java.util.function.Consumer;
*/
public class QuadTree<T>
{
public static final byte TREE_LOWEST_DETAIL_LEVEL = 0;
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** The largest number detail level in this tree. */
public final byte treeMaxDetailLevel;
/** The smallest number detail level in this tree. */
public final byte treeMinDetailLevel;
/** contain the actual data in the quad tree structure */
private final MovableGridRingList<QuadNode<T>> topRingList;
@@ -42,13 +42,14 @@ public class QuadTree<T>
* Constructor of the quadTree
* @param widthInBlocks equivalent to the distance between two opposing sides
*/
public QuadTree(int widthInBlocks, DhBlockPos2D centerBlockPos)
public QuadTree(int widthInBlocks, DhBlockPos2D centerBlockPos, byte treeMinDetailLevel)
{
DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else
this.centerBlockPos = centerBlockPos;
this.widthInBlocks = widthInBlocks;
this.treeMaxDetailLevel = 10; // TODO in the future we may need to make this dynamic // detail 10 = (2^10) 1024 blocks wide
this.treeMinDetailLevel = treeMinDetailLevel;
int halfSizeInRootNodes = Math.floorDiv(this.widthInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel);
halfSizeInRootNodes = halfSizeInRootNodes + 1; // always add 1 so nodes will always have a parent, even if the tree's center is offset from the root node grid
@@ -87,7 +88,7 @@ public class QuadTree<T>
return null;
}
topQuadNode = new QuadNode<T>(rootPos);
topQuadNode = new QuadNode<T>(rootPos, this.treeMinDetailLevel);
boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode);
LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: "+rootPos);
}
@@ -110,12 +111,21 @@ public class QuadTree<T>
int radius = this.diameterInBlocks()/2;
DhBlockPos2D minPos = this.getCenterBlockPos().add(new DhBlockPos2D(-radius, -radius));
DhBlockPos2D maxPos =this.getCenterBlockPos().add(new DhBlockPos2D(radius, radius));
throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: "+minPos+", max pos: "+maxPos+", given Position: "+pos+" = block pos: "+pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL));
throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: "+minPos+", max pos: "+maxPos+", min detail level: "+this.treeMinDetailLevel+", max detail level: "+this.treeMaxDetailLevel+". Given Position: "+pos+" = block pos: "+pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL));
}
}
private boolean isSectionPosInBounds(DhSectionPos testPos)
public boolean isSectionPosInBounds(DhSectionPos testPos)
{
// check if the testPos is within the detail level limits of the tree
boolean detailLevelWithinBounds = this.treeMinDetailLevel <= testPos.sectionDetailLevel && testPos.sectionDetailLevel <= this.treeMaxDetailLevel;
if (!detailLevelWithinBounds)
{
return false;
}
// check if the testPos is within the X,Z boundry of the tree
DhBlockPos2D blockCornerOfTree = this.centerBlockPos.add(new DhBlockPos2D(-this.widthInBlocks/2,-this.widthInBlocks/2));
DhLodPos cornerOfTreePos = new DhLodPos((byte)0, blockCornerOfTree.x, blockCornerOfTree.z);
@@ -125,7 +135,7 @@ public class QuadTree<T>
return DoSquaresOverlap(cornerOfTreePos, this.widthInBlocks, cornerOfInputPos, inputWidth);
}
public static boolean DoSquaresOverlap(DhLodPos rect1Min, int rect1Width, DhLodPos rect2Min, int rect2Width)
private static boolean DoSquaresOverlap(DhLodPos rect1Min, int rect1Width, DhLodPos rect2Min, int rect2Width)
{
// Determine the coordinates of the rectangles
float rect1MinX = rect1Min.x;
@@ -157,13 +167,14 @@ public class QuadTree<T>
}
/** root nodes can be null */
public void forEachRootNodePos(BiConsumer<QuadNode<T>, Pos2D> consumer)
public void forEachRootNodePos(BiConsumer<QuadNode<T>, DhSectionPos> consumer)
{
this.topRingList.forEachPosOrdered((rootNode, pos2D) ->
{
if (isSectionPosInBounds(new DhSectionPos(this.treeMaxDetailLevel, pos2D.x, pos2D.y)))
DhSectionPos rootPos = new DhSectionPos(this.treeMaxDetailLevel, pos2D.x, pos2D.y);
if (isSectionPosInBounds(rootPos))
{
consumer.accept(rootNode, pos2D);
consumer.accept(rootNode, rootPos);
}
});
}
+110 -20
View File
@@ -24,6 +24,7 @@ import com.seibel.lod.core.pos.DhBlockPos2D;
import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.util.BitShiftUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.objects.quadTree.QuadNode;
import com.seibel.lod.core.util.objects.quadTree.QuadTree;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
@@ -31,6 +32,7 @@ import org.apache.logging.log4j.core.config.Configurator;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class QuadTreeTest
@@ -59,7 +61,7 @@ public class QuadTreeTest
@Test
public void BasicPositiveQuadTreeTest()
{
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("Incorrect basic tree width", BASIC_TREE_ACTUAL_WIDTH_IN_ROOT_NODES, tree.ringListWidth());
@@ -94,7 +96,7 @@ public class QuadTreeTest
@Test
public void BasicNegativeQuadTreeTest()
{
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
// root node //
@@ -129,7 +131,7 @@ public class QuadTreeTest
@Test
public void OutOfBoundsQuadTreeTest()
{
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0,0));
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0,0), LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("tree diameter incorrect", BASIC_TREE_WIDTH_IN_BLOCKS, tree.diameterInBlocks());
@@ -174,7 +176,7 @@ public class QuadTreeTest
{
int treeWidthInRootNodes = 8;
int treeWidthInBlocks = ROOT_NODE_WIDTH_IN_BLOCKS * treeWidthInRootNodes;
QuadTree<Integer> tree = new QuadTree<>(treeWidthInBlocks, TREE_CENTER_POS);
QuadTree<Integer> tree = new QuadTree<>(treeWidthInBlocks, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
// root nodes //
@@ -261,7 +263,7 @@ public class QuadTreeTest
@Test
public void QuadTreeIterationTest()
{
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
QuadTree<Integer> tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
// root nodes //
@@ -298,7 +300,7 @@ public class QuadTreeTest
@Test
public void CenteredGridListIterationTest()
{
final QuadTree<Integer> tree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
final QuadTree<Integer> tree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 0), 0);
// confirm the root node were added
@@ -308,10 +310,8 @@ public class QuadTreeTest
// attempt to get and remove, each node in the tree
final AtomicInteger rootNodePosCount = new AtomicInteger(0);
tree.forEachRootNodePos((renderBufferNode, pos2d) ->
tree.forEachRootNodePos((renderBufferNode, sectionPos) ->
{
DhSectionPos sectionPos = new DhSectionPos(tree.treeMaxDetailLevel, pos2d.x, pos2d.y);
testGet(tree, sectionPos, 0);
testSet(tree, sectionPos, null);
@@ -326,14 +326,14 @@ public class QuadTreeTest
{
// offset fully inside (10*0,0)
final QuadTree<Integer> fullyInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
final QuadTree<Integer> fullyInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
DhBlockPos2D fullyInsideOffsetBlockPos = new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS, MINIMUM_TREE_WIDTH_IN_BLOCKS);
fullyInsideTree.setCenterBlockPos(fullyInsideOffsetBlockPos);
fullyInsideTree.forEachRootNodePos((rootNode, pos2D) ->
fullyInsideTree.forEachRootNodePos((rootNode, sectionPos) ->
{
testSet(fullyInsideTree, new DhSectionPos(fullyInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0);
testSet(fullyInsideTree, sectionPos, 0);
});
// only 1 root node should be added
@@ -345,11 +345,11 @@ public class QuadTreeTest
// offset fully inside (10*0,0)
final QuadTree<Integer> borderInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS * 2, MINIMUM_TREE_WIDTH_IN_BLOCKS * 2));
final QuadTree<Integer> borderInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS * 2, MINIMUM_TREE_WIDTH_IN_BLOCKS * 2), LodUtil.BLOCK_DETAIL_LEVEL);
borderInsideTree.forEachRootNodePos((rootNode, pos2D) ->
borderInsideTree.forEachRootNodePos((rootNode, sectionPos) ->
{
testSet(borderInsideTree, new DhSectionPos(borderInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0);
testSet(borderInsideTree, sectionPos, 0);
});
// only 1 root node should be added
@@ -361,14 +361,14 @@ public class QuadTreeTest
// offset across (10*-1,0) and (10*0,0)
final QuadTree<Integer> acrossTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
final QuadTree<Integer> acrossTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
DhBlockPos2D acrossOffsetBlockPos = new DhBlockPos2D(-MINIMUM_TREE_WIDTH_IN_BLOCKS/4, MINIMUM_TREE_WIDTH_IN_BLOCKS);
acrossTree.setCenterBlockPos(acrossOffsetBlockPos);
acrossTree.forEachRootNodePos((rootNode, pos2D) ->
acrossTree.forEachRootNodePos((rootNode, sectionPos) ->
{
testSet(acrossTree, new DhSectionPos(acrossTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0);
testSet(acrossTree, sectionPos, 0);
});
// 2 root nodes should be added
@@ -381,7 +381,7 @@ public class QuadTreeTest
@Test
public void TinyGridAlignedTreeTest()
{
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, TREE_CENTER_POS);
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", ROOT_NODE_WIDTH_IN_BLOCKS, tree.diameterInBlocks());
@@ -404,7 +404,7 @@ public class QuadTreeTest
@Test
public void TinyGridOffsetTreeTest()
{
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0));
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", ROOT_NODE_WIDTH_IN_BLOCKS, tree.diameterInBlocks());
@@ -434,6 +434,96 @@ public class QuadTreeTest
}
@Test
public void TreeDetailLevelLimitTest()
{
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), (byte)8);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMaxDetailLevel);
// valid detail levels
testSet(tree, new DhSectionPos((byte)10, 0, 0), 1);
testSet(tree, new DhSectionPos((byte)9, 0, 0), 2);
testSet(tree, new DhSectionPos((byte)8, 0, 0), 3);
// detail level too low
testSet(tree, new DhSectionPos((byte)7, 0, 0), -1, IndexOutOfBoundsException.class);
testSet(tree, new DhSectionPos((byte)6, 0, 0), -1, IndexOutOfBoundsException.class);
// detail level too high
testSet(tree, new DhSectionPos((byte)11, 0, 0), -1, IndexOutOfBoundsException.class);
testSet(tree, new DhSectionPos((byte)12, 0, 0), -1, IndexOutOfBoundsException.class);
}
@Test
public void QuadNodeDetailLimitTest()
{
QuadTree<Integer> tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), (byte)6);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMaxDetailLevel);
// create the root node
testSet(tree, new DhSectionPos((byte)10, 0, 0), 1);
// recurse down the tree
AtomicInteger minimumDetailLevelReachedRef = new AtomicInteger(tree.treeMaxDetailLevel);
tree.forEachRootNode((rootNode) ->
{
rootNode.forEachDirectChild((quadNode, sectionPos) ->
{
// all sections will be null
rootNode.setValue(sectionPos, 0);
});
rootNode.forEachDirectChild((quadNode, sectionPos) ->
{
recursivelyCreateNodeChildren(quadNode, tree.treeMinDetailLevel, minimumDetailLevelReachedRef);
});
});
// confirm that the tree can and did iterate all the way down to the minimum detail level
Assert.assertEquals("Minimum detail level never reached", minimumDetailLevelReachedRef.get(), tree.treeMinDetailLevel);
}
private void recursivelyCreateNodeChildren(QuadNode<Integer> node, byte minDetailLevel, AtomicInteger minimumDetailLevelReachedRef)
{
AtomicBoolean childNodesCreatedRef = new AtomicBoolean(false);
AtomicBoolean childNodesIteratedRef = new AtomicBoolean(false);
// fill in the null children
node.forEachDirectChild((childNode, childSectionPos) ->
{
node.setValue(childSectionPos, 0);
childNodesCreatedRef.set(true);
});
// attempt to recurse down these new children
node.forEachDirectChild((childNode, childSectionPos) ->
{
Assert.assertTrue("Child node recurred too low. Min detail level: "+minDetailLevel+", node detail level: "+childSectionPos.sectionDetailLevel, childSectionPos.sectionDetailLevel >= minDetailLevel);
recursivelyCreateNodeChildren(childNode, minDetailLevel, minimumDetailLevelReachedRef);
childNodesIteratedRef.set(true);
});
// keep track of how far down the tree we have gone
if (node.sectionPos.sectionDetailLevel < minimumDetailLevelReachedRef.get())
{
minimumDetailLevelReachedRef.set(node.sectionPos.sectionDetailLevel);
}
// assertions
if (childNodesCreatedRef.get())
{
Assert.assertTrue("node children created below minimum detail level", node.sectionPos.sectionDetailLevel >= minDetailLevel);
}
if (childNodesIteratedRef.get())
{
Assert.assertTrue("node children iterated below minimum detail level", node.sectionPos.sectionDetailLevel-1 >= minDetailLevel);
}
}