diff --git a/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java b/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java index 693420169..18722da06 100644 --- a/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java +++ b/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java @@ -83,12 +83,22 @@ public class DhSectionPos LodUtil.assertTrue(returnDetailLevel <= this.sectionDetailLevel, "returnDetailLevel must be less than sectionDetail"); if (returnDetailLevel == this.sectionDetailLevel) + { return new DhLodPos(this.sectionDetailLevel, this.sectionX, this.sectionZ); + } + + byte detailLevelOffset = (byte) (this.sectionDetailLevel - returnDetailLevel); + + // we can't get the center of the position at block level, only attempt to get the position offset for detail levels above 0 // TODO should this also apply to detail level 1 or is it fine? + int positionOffset = 0; + if (this.sectionDetailLevel != 1 || returnDetailLevel != 0) + { + positionOffset = BitShiftUtil.powerOfTwo(detailLevelOffset - 1); + } - byte offset = (byte) (this.sectionDetailLevel - returnDetailLevel); return new DhLodPos(returnDetailLevel, - (this.sectionX * BitShiftUtil.powerOfTwo(offset)) + BitShiftUtil.powerOfTwo(offset - 1), - (this.sectionZ * BitShiftUtil.powerOfTwo(offset)) + BitShiftUtil.powerOfTwo(offset - 1)); + (this.sectionX * BitShiftUtil.powerOfTwo(detailLevelOffset)) + positionOffset, + (this.sectionZ * BitShiftUtil.powerOfTwo(detailLevelOffset)) + positionOffset); } /** @return the corner with the smallest X and Z coordinate */ @@ -111,7 +121,10 @@ public class DhSectionPos return new DhLodUnit(this.sectionDetailLevel, BitShiftUtil.powerOfTwo(offset)); } - /** uses the absolute detail level aka detail levels like {@link LodUtil#CHUNK_DETAIL_LEVEL} instead of the dhSectionPos detaillevels */ // TODO comment + /** + * uses the absolute detail level aka detail levels like {@link LodUtil#CHUNK_DETAIL_LEVEL} instead of the dhSectionPos detailLevels + * @return the new position closest to negative infinity with the new detail level + */ public DhSectionPos convertToDetailLevel(byte newSectionDetailLevel) { DhLodPos lodPos = new DhLodPos(this.sectionDetailLevel, this.sectionX, this.sectionZ); @@ -125,10 +138,10 @@ public class DhSectionPos * Returns the DhLodPos 1 detail level lower

* * Relative child positions returned for each index:
- * 0 = (0,0)
- * 1 = (1,0)
- * 2 = (0,1)
- * 3 = (1,1)
+ * 0 = (0,0) - North West
+ * 1 = (1,0) - South West
+ * 2 = (0,1) - North East
+ * 3 = (1,1) - South East
* * @param child0to3 must be an int between 0 and 3 */ @@ -172,10 +185,11 @@ public class DhSectionPos /** NOTE: This does not consider yOffset! */ public boolean contains(DhSectionPos otherPos) { + DhBlockPos2D thisMinBlockPos = this.getCorner(LodUtil.BLOCK_DETAIL_LEVEL).getCornerBlockPos(); DhBlockPos2D otherCornerBlockPos = otherPos.getCorner(LodUtil.BLOCK_DETAIL_LEVEL).getCornerBlockPos(); - DhBlockPos2D thisMinBlockPos = this.getCorner(LodUtil.BLOCK_DETAIL_LEVEL).getCornerBlockPos(); - DhBlockPos2D thisMaxBlockPos = new DhBlockPos2D(thisMinBlockPos.x + this.getWidth().toBlockWidth(), thisMinBlockPos.z + this.getWidth().toBlockWidth()); + int thisBlockWidth = this.getWidth().toBlockWidth() - 1; // minus 1 to account for zero based positional indexing + DhBlockPos2D thisMaxBlockPos = new DhBlockPos2D(thisMinBlockPos.x + thisBlockWidth, thisMinBlockPos.z + thisBlockWidth); return thisMinBlockPos.x <= otherCornerBlockPos.x && otherCornerBlockPos.x <= thisMaxBlockPos.x && thisMinBlockPos.z <= otherCornerBlockPos.z && otherCornerBlockPos.z <= thisMaxBlockPos.z; diff --git a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java index f2478876a..2b5b1dbc5 100644 --- a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java +++ b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java @@ -1,8 +1,8 @@ package com.seibel.lod.core.util.objects.quadTree; import com.seibel.lod.core.logging.DhLoggerBuilder; -import com.seibel.lod.core.pos.DhLodPos; import com.seibel.lod.core.pos.DhSectionPos; +import com.seibel.lod.core.util.LodUtil; import org.apache.logging.log4j.Logger; import java.util.function.Consumer; @@ -119,7 +119,7 @@ public class QuadNode 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()); - throw new IllegalArgumentException("Input section pos outside of this quadNode's range: "+this.sectionPos+" width: "+this.sectionPos.getWidth()+" input detail level: "+inputSectionPos+" width: "+inputSectionPos.getWidth()); + throw new IllegalArgumentException("Input section pos "+inputSectionPos+" outside of this quadNode's pos: "+this.sectionPos+", this node's blockPos: "+this.sectionPos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL)+" block width: "+this.sectionPos.getWidth().toBlockWidth()+" input detail level: "+inputSectionPos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL)+" width: "+inputSectionPos.getWidth().toBlockWidth()); } if (inputSectionPos.sectionDetailLevel > this.sectionPos.sectionDetailLevel) @@ -149,65 +149,68 @@ public class QuadNode // LOGGER.info((replaceValue ? "set " : "get ")+inputSectionPos+" center block: "+inputSectionPos.getCenter().getCornerBlockPos()+", this pos: "+this.sectionPos+" this center block: "+this.sectionPos.getCenter().getCornerBlockPos()); - DhLodPos nodeCenterPos = this.sectionPos.getCenter(); //.convertToDetailLevel((byte)0).getCenter(); - DhLodPos inputCenterPos = inputSectionPos.getCenter(); //.convertToDetailLevel((byte)0).getCenter(); + DhSectionPos nwPos = this.sectionPos.getChildByIndex(0); + DhSectionPos swPos = this.sectionPos.getChildByIndex(1); + DhSectionPos nePos = this.sectionPos.getChildByIndex(2); + DhSectionPos sePos = this.sectionPos.getChildByIndex(3); - // may or may not be at the requested detail level + // look for the child that contains the input position (there may be a faster way to do this, but this works for now) QuadNode childNode; - if (inputCenterPos.x <= nodeCenterPos.x) + if (nwPos.contains(inputSectionPos)) { - if (inputCenterPos.z <= nodeCenterPos.z) + // TODO merge duplicate code + if (replaceValue && this.nwChild == null) { - // TODO merge duplicate code - 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<>(this.sectionPos.getChildByIndex(0)); - } -// LOGGER.info("NW"); - childNode = this.nwChild; + // 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); } - else + childNode = this.nwChild; + + // childNode should only be null when replaceValue = false and the end of a node chain has been reached + return (childNode != null) ? childNode.getOrSetValue(inputSectionPos, replaceValue, newValue) : null; + } + else if (swPos.contains(inputSectionPos)) + { + // TODO merge duplicate code + if (replaceValue && this.swChild == null) { - if (replaceValue && this.neChild == null) - { - this.neChild = new QuadNode<>(this.sectionPos.getChildByIndex(2)); - } -// LOGGER.info("NE"); - childNode = this.neChild; + // 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); } + childNode = this.swChild; + + // childNode should only be null when replaceValue = false and the end of a node chain has been reached + return (childNode != null) ? childNode.getOrSetValue(inputSectionPos, replaceValue, newValue) : null; + } + else if (nePos.contains(inputSectionPos)) + { + // TODO merge duplicate code + 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); + } + childNode = this.neChild; + + // childNode should only be null when replaceValue = false and the end of a node chain has been reached + return (childNode != null) ? childNode.getOrSetValue(inputSectionPos, replaceValue, newValue) : null; + } + else if (sePos.contains(inputSectionPos)) + { + // TODO merge duplicate code + 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); + } + childNode = this.seChild; + + // childNode should only be null when replaceValue = false and the end of a node chain has been reached + return (childNode != null) ? childNode.getOrSetValue(inputSectionPos, replaceValue, newValue) : null; } else { - if (inputCenterPos.z <= nodeCenterPos.z) - { - if (replaceValue && this.swChild == null) - { - this.swChild = new QuadNode<>(this.sectionPos.getChildByIndex(1)); - } -// LOGGER.info("SW"); - childNode = this.swChild; - } - else - { - if (replaceValue && this.seChild == null) - { - this.seChild = new QuadNode<>(this.sectionPos.getChildByIndex(3)); - } -// LOGGER.info("SE"); - childNode = this.seChild; - } - } - - - if (childNode == null) - { - // should only happen when replaceValue = false and the end of a node chain has been reached - return null; - } - else - { - return childNode.getOrSetValue(inputSectionPos, replaceValue, newValue); + throw new IllegalStateException("input position not contained by any node children. This should've been caught by the this.sectionPos.contains(inputPos) assert before this point."); } } } diff --git a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java index 5eb2c3d4d..0998e86e2 100644 --- a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java +++ b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java @@ -2,6 +2,7 @@ package com.seibel.lod.core.util.objects.quadTree; import com.seibel.lod.core.logging.DhLoggerBuilder; import com.seibel.lod.core.pos.DhBlockPos2D; +import com.seibel.lod.core.pos.DhLodPos; import com.seibel.lod.core.pos.DhSectionPos; import com.seibel.lod.core.pos.Pos2D; import com.seibel.lod.core.util.BitShiftUtil; @@ -11,6 +12,7 @@ import com.seibel.lod.core.util.gridList.MovableGridRingList; import org.apache.logging.log4j.Logger; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -30,7 +32,9 @@ public class QuadTree /** contain the actual data in the quad tree structure */ private final MovableGridRingList> topRingList; - DhBlockPos2D centerBlockPos; + private DhBlockPos2D centerBlockPos; + + private int widthInBlocks; @@ -42,20 +46,19 @@ public class QuadTree { DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else this.centerBlockPos = centerBlockPos; + this.widthInBlocks = widthInBlocks; - this.treeMaxDetailLevel = 10; // TODO we may need to make this dynamic // detail 10 = (2^10) 1024 blocks wide + this.treeMaxDetailLevel = 10; // TODO in the future we may need to make this dynamic // detail 10 = (2^10) 1024 blocks wide -// int halfSize = 12; // TODO use this.treeMaxDetailLevel to determine - int halfSize = Math.floorDiv(widthInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel); - halfSize = Math.max(halfSize, 1); // at minimum the ring list should have 3x3 (9) root nodes in it, to account for moving around + 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 Pos2D ringListCenterPos = new Pos2D( BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMaxDetailLevel), BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMaxDetailLevel)); - - this.topRingList = new MovableGridRingList<>(halfSize, ringListCenterPos.x, ringListCenterPos.y); + this.topRingList = new MovableGridRingList<>(halfSizeInRootNodes, ringListCenterPos.x, ringListCenterPos.y); - }// constructor + } @@ -70,7 +73,7 @@ public class QuadTree protected final T getOrSet(DhSectionPos pos, boolean setNewValue, T newValue) throws IndexOutOfBoundsException { - if (this.isPositionInBounds(pos)) + if (this.isSectionPosInBounds(pos)) { DhSectionPos rootPos = pos.convertToDetailLevel(this.treeMaxDetailLevel); int ringListPosX = rootPos.sectionX; @@ -79,6 +82,11 @@ public class QuadTree QuadNode topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ); if (topQuadNode == null) { + if (!setNewValue) + { + return null; + } + topQuadNode = new QuadNode(rootPos); boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: "+rootPos); @@ -99,34 +107,44 @@ public class QuadTree } else { - // TODO give the min and max allowed positions - int width = this.widthInBlocks()/2; - - DhBlockPos2D minPos = this.getCenterBlockPos().add(new DhBlockPos2D(-width, -width)); - DhBlockPos2D maxPos =this.getCenterBlockPos().add(new DhBlockPos2D(width, width)); - throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: "+minPos+", max pos: "+maxPos+", given Position: "+pos); + 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)); } } - - private boolean isPositionInBounds(DhSectionPos pos) + private boolean isSectionPosInBounds(DhSectionPos testPos) { - DhSectionPos blockPos = pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL); + DhBlockPos2D blockCornerOfTree = this.centerBlockPos.add(new DhBlockPos2D(-this.widthInBlocks/2,-this.widthInBlocks/2)); + DhLodPos cornerOfTreePos = new DhLodPos((byte)0, blockCornerOfTree.x, blockCornerOfTree.z); - int halfWidthInBlocks = BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel) * Math.floorDiv(this.topRingList.getWidth(), 2); + DhSectionPos sectionCornerOfInput = testPos.convertToDetailLevel((byte)0); + DhLodPos cornerOfInputPos = new DhLodPos((byte)0, sectionCornerOfInput.sectionX, sectionCornerOfInput.sectionZ); + int inputWidth = BitShiftUtil.powerOfTwo(testPos.sectionDetailLevel); - int minX = this.centerBlockPos.x - halfWidthInBlocks; - int maxX = this.centerBlockPos.x + halfWidthInBlocks; + return DoSquaresOverlap(cornerOfTreePos, this.widthInBlocks, cornerOfInputPos, inputWidth); + } + public static boolean DoSquaresOverlap(DhLodPos rect1Min, int rect1Width, DhLodPos rect2Min, int rect2Width) + { + // Determine the coordinates of the rectangles + float rect1MinX = rect1Min.x; + float rect1MaxX = rect1Min.x + rect1Width; + float rect1MinZ = rect1Min.z; + float rect1MaxZ = rect1Min.z + rect1Width; - int minZ = this.centerBlockPos.z - halfWidthInBlocks; - int maxZ = this.centerBlockPos.z + halfWidthInBlocks; + float rect2MinX = rect2Min.x; + float rect2MaxX = rect2Min.x + rect2Width; + float rect2MinZ = rect2Min.z; + float rect2MaxZ = rect2Min.z + rect2Width; - return minX <= blockPos.sectionX && blockPos.sectionX <= maxX && - minZ <= blockPos.sectionZ && blockPos.sectionZ <= maxZ; + // Check if the rectangles overlap + return rect1MinX < rect2MaxX && rect1MaxX > rect2MinX && rect1MinZ < rect2MaxZ && rect1MaxZ > rect2MinZ; } - /** no nulls TODO */ + + /** no nulls TODO comment/rename */ public void forEachRootNode(Consumer> consumer) { this.topRingList.forEachOrdered((rootNode) -> @@ -138,6 +156,18 @@ public class QuadTree }); } + /** root nodes can be null */ + public void forEachRootNodePos(BiConsumer, Pos2D> consumer) + { + this.topRingList.forEachPosOrdered((rootNode, pos2D) -> + { + if (isSectionPosInBounds(new DhSectionPos(this.treeMaxDetailLevel, pos2D.x, pos2D.y))) + { + consumer.accept(rootNode, pos2D); + } + }); + } + public void forEachLeafValue(Consumer consumer) { this.forEachRootNode((rootNode) -> @@ -189,19 +219,16 @@ public class QuadTree public int leafNodeCount() { AtomicInteger count = new AtomicInteger(0); - this.topRingList.forEachPos((node, pos) -> + this.topRingList.forEachOrdered((node) -> { - if (node != null) - { - node.forAllLeafValues((value) -> { count.addAndGet(1); }); - } + node.forAllLeafValues((value) -> { count.addAndGet(1); }); }); return count.get(); } public int ringListWidth() { return this.topRingList.getWidth(); } - public int widthInBlocks() { return this.ringListWidth() * BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel); } + public int diameterInBlocks() { return this.widthInBlocks; } // public String getDebugString() // { diff --git a/core/src/test/java/tests/DhSectionPosTest.java b/core/src/test/java/tests/DhSectionPosTest.java index 755abe118..a25368b10 100644 --- a/core/src/test/java/tests/DhSectionPosTest.java +++ b/core/src/test/java/tests/DhSectionPosTest.java @@ -21,6 +21,7 @@ package tests; import com.seibel.lod.core.logging.DhLoggerBuilder; import com.seibel.lod.core.pos.DhBlockPos2D; +import com.seibel.lod.core.pos.DhLodPos; import com.seibel.lod.core.pos.DhSectionPos; import com.seibel.lod.core.util.BitShiftUtil; import com.seibel.lod.core.util.LodUtil; @@ -36,7 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class DhSectionPosTest { @Test - public void SectionPosTest() + public void ContainsPosTest() { DhSectionPos root = new DhSectionPos((byte)10, 0, 0); DhSectionPos child = new DhSectionPos((byte)9, 1, 1); @@ -46,10 +47,125 @@ public class DhSectionPosTest root = new DhSectionPos((byte)10, 1, 0); + + // out of bounds + child = new DhSectionPos((byte)9, 0, 0); + Assert.assertFalse("position should be out of bounds", root.contains(child)); child = new DhSectionPos((byte)9, 1, 1); - Assert.assertFalse("section pos contains fail", root.contains(child)); + Assert.assertFalse("position should be out of bounds", root.contains(child)); + + // in bounds + child = new DhSectionPos((byte)9, 2, 0); + Assert.assertTrue("position should be in bounds", root.contains(child)); + child = new DhSectionPos((byte)9, 3, 1); + Assert.assertTrue("position should be in bounds", root.contains(child)); + + // out of bounds child = new DhSectionPos((byte)9, 2, 2); - Assert.assertTrue("section pos contains fail", root.contains(child)); + Assert.assertFalse("position should be out of bounds", root.contains(child)); + child = new DhSectionPos((byte)9, 3, 3); + Assert.assertFalse("position should be out of bounds", root.contains(child)); + + child = new DhSectionPos((byte)9, 4, 4); + Assert.assertFalse("position should be out of bounds", root.contains(child)); + child = new DhSectionPos((byte)9, 5, 5); + Assert.assertFalse("position should be out of bounds", root.contains(child)); + } + + @Test + public void ContainsAdjacentPosTest() + { + // neither should contain the other, they are single blocks that are next to each other + DhSectionPos left = new DhSectionPos((byte)0, 4606, 0); + DhSectionPos right = new DhSectionPos((byte)0, 4607, 0); + Assert.assertFalse(left.contains(right)); + Assert.assertFalse(right.contains(left)); + + + // 512 block wide sections that are adjacent, but not overlapping + left = new DhSectionPos((byte)9, 0, 0); + right = new DhSectionPos((byte)9, 1, 0); + Assert.assertFalse(left.contains(right)); + Assert.assertFalse(right.contains(left)); + + } + + @Test + public void ParentPosTest() + { + DhSectionPos leaf = new DhSectionPos((byte)0, 0, 0); + DhSectionPos convert = leaf.convertToDetailLevel((byte)1); + DhSectionPos parent = leaf.getParentPos(); + Assert.assertEquals("get parent at 0,0 fail", convert, parent); + + + leaf = new DhSectionPos((byte)0, 1, 1); + convert = leaf.convertToDetailLevel((byte)1); + parent = leaf.getParentPos(); + Assert.assertEquals("get parent at 1,1 fail", convert, parent); + + + leaf = new DhSectionPos((byte)1, 2, 2); + convert = leaf.convertToDetailLevel((byte)2); + parent = leaf.getParentPos(); + Assert.assertEquals("parent upscale fail", convert, parent); + convert = leaf.convertToDetailLevel((byte)0); + DhSectionPos childIndex = leaf.getChildByIndex(0); + Assert.assertEquals("child detail fail", convert, childIndex); + + } + + @Test + public void ChildPosTest() + { + DhSectionPos node = new DhSectionPos((byte)1, 2302, 0); + DhSectionPos nw = node.getChildByIndex(0); + DhSectionPos sw = node.getChildByIndex(1); + DhSectionPos ne = node.getChildByIndex(2); + DhSectionPos se = node.getChildByIndex(3); + + // confirm no children have the same values + Assert.assertNotEquals(nw, sw); + Assert.assertNotEquals(sw, ne); + Assert.assertNotEquals(ne, se); + + // confirm each child has the correct value + Assert.assertEquals(nw, new DhSectionPos((byte)0, 4604, 0)); + Assert.assertEquals(sw, new DhSectionPos((byte)0, 4605, 0)); + Assert.assertEquals(ne, new DhSectionPos((byte)0, 4604, 1)); + Assert.assertEquals(se, new DhSectionPos((byte)0, 4605, 1)); + + } + + @Test + public void GetCenterTest() + { + DhSectionPos node = new DhSectionPos((byte)1, 2303, 0); + DhLodPos centerNode = node.getCenter(); + DhLodPos expectedCenterNode = new DhLodPos((byte)0, 4606,0); + Assert.assertEquals("", expectedCenterNode, centerNode); + + + + node = new DhSectionPos((byte)10, 0, 0); // 1024 blocks wide + centerNode = node.getCenter(); + expectedCenterNode = new DhLodPos((byte)0, 1024/2,1024/2); + Assert.assertEquals("", expectedCenterNode, centerNode); + + } + + @Test + public void GetCenter2Test() + { + DhSectionPos parentNode = new DhSectionPos((byte)2, 1151, 0); // width 4 blocks + DhSectionPos inputPos = new DhSectionPos((byte)0, 4606, 0); // width 1 block + Assert.assertTrue(parentNode.contains(inputPos)); + + DhLodPos parentCenter = parentNode.getCenter(); + DhLodPos inputCenter = inputPos.getCenter(); + + Assert.assertEquals(new DhLodPos((byte)0, 4606, 2), parentCenter); + Assert.assertEquals(new DhLodPos((byte)0, 4606, 0), inputCenter); } diff --git a/core/src/test/java/tests/QuadTreeTest.java b/core/src/test/java/tests/QuadTreeTest.java index 7c4b4d1b2..735eb5d4e 100644 --- a/core/src/test/java/tests/QuadTreeTest.java +++ b/core/src/test/java/tests/QuadTreeTest.java @@ -37,10 +37,17 @@ public class QuadTreeTest { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final DhBlockPos2D TREE_CENTER_POS = new DhBlockPos2D(BitShiftUtil.powerOfTwo(10)/2, BitShiftUtil.powerOfTwo(10)/2); + private static final int ROOT_NODE_WIDTH_IN_BLOCKS = BitShiftUtil.powerOfTwo(10); /** needs to be an odd number to function correctly */ - private static final int BASIC_TREE_WIDTH_IN_ROOT_NODES = 9; - private static final int BASIC_TREE_WIDTH_IN_BLOCKS = ROOT_NODE_WIDTH_IN_BLOCKS * BASIC_TREE_WIDTH_IN_ROOT_NODES; + private static final int BASIC_TREE_INPUT_WIDTH_IN_ROOT_NODES = 9; + private static final int BASIC_TREE_WIDTH_IN_BLOCKS = ROOT_NODE_WIDTH_IN_BLOCKS * BASIC_TREE_INPUT_WIDTH_IN_ROOT_NODES; + + /** the tree should be slightly larger to account for offset centers */ + private static final int BASIC_TREE_ACTUAL_WIDTH_IN_ROOT_NODES = BASIC_TREE_INPUT_WIDTH_IN_ROOT_NODES + 2; + + private static final int MINIMUM_TREE_WIDTH_IN_BLOCKS = 32; static { @@ -52,8 +59,8 @@ public class QuadTreeTest @Test public void BasicPositiveQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); - Assert.assertEquals("Incorrect basic tree width", BASIC_TREE_WIDTH_IN_ROOT_NODES, tree.ringListWidth()); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + Assert.assertEquals("Incorrect basic tree width", BASIC_TREE_ACTUAL_WIDTH_IN_ROOT_NODES, tree.ringListWidth()); // root node // @@ -87,7 +94,7 @@ public class QuadTreeTest @Test public void BasicNegativeQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); // root node // @@ -122,21 +129,52 @@ public class QuadTreeTest @Test public void OutOfBoundsQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0,0)); + Assert.assertEquals("tree diameter incorrect", BASIC_TREE_WIDTH_IN_BLOCKS, tree.diameterInBlocks()); + // wrong detail level on purpose, if the detail level was 0 (block) this should work DhSectionPos outOfBoundsPos = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, ROOT_NODE_WIDTH_IN_BLOCKS, 0); - testSet(tree, outOfBoundsPos, 2, IndexOutOfBoundsException.class); + testSet(tree, outOfBoundsPos, -1, IndexOutOfBoundsException.class); Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount()); + + // out of bounds // + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2) + 1, 0); + testSet(tree, outOfBoundsPos, -1, IndexOutOfBoundsException.class); + Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount()); + + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2), 0); + testSet(tree, outOfBoundsPos, -1, IndexOutOfBoundsException.class); + Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount()); + + + // in bounds // + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2)-1, 0); + testSet(tree, outOfBoundsPos, 0); + Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount()); + + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2)-3, 0); + testSet(tree, outOfBoundsPos, 0); + Assert.assertEquals("incorrect leaf node count", 2, tree.leafNodeCount()); + + // TODO this position probably has trouble with getting the center. + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2)-2, 0); + testSet(tree, outOfBoundsPos, 0); + Assert.assertEquals("incorrect leaf node count", 3, tree.leafNodeCount()); + + outOfBoundsPos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, (BASIC_TREE_WIDTH_IN_BLOCKS/2)-4, 0); + testSet(tree, outOfBoundsPos, 0); + Assert.assertEquals("incorrect leaf node count", 4, tree.leafNodeCount()); + } @Test - public void QuadTreeMovingTest() + public void QuadTreeRootAlignedMovingTest() { int treeWidthInRootNodes = 8; int treeWidthInBlocks = ROOT_NODE_WIDTH_IN_BLOCKS * treeWidthInRootNodes; - QuadTree tree = new QuadTree<>(treeWidthInBlocks, new DhBlockPos2D(0, 0)); + QuadTree tree = new QuadTree<>(treeWidthInBlocks, TREE_CENTER_POS); // root nodes // @@ -212,18 +250,18 @@ public class QuadTreeTest Assert.assertEquals("incorrect leaf node count", 2, tree.leafNodeCount()); // move so only the root nodes exactly on the X edge remain - DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS - (BASIC_TREE_WIDTH_IN_ROOT_NODES*ROOT_NODE_WIDTH_IN_BLOCKS), 0); + DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS - (BASIC_TREE_INPUT_WIDTH_IN_ROOT_NODES*ROOT_NODE_WIDTH_IN_BLOCKS), 0); tree.setCenterBlockPos(edgeMoveBlockPos); Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos()); - Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount()); + Assert.assertEquals("incorrect leaf node count", 2, tree.leafNodeCount()); } @Test public void QuadTreeIterationTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); // root nodes // @@ -258,25 +296,133 @@ public class QuadTreeTest } @Test - public void FullTreeTest() + public void CenteredGridListIterationTest() { - QuadTree tree = new QuadTree<>(0, new DhBlockPos2D(0, 0)); - // minimum size tree should be 3 root nodes wide - Assert.assertEquals("incorrect minimum size tree", 3, tree.ringListWidth()); - - - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, -1), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, 0), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, 1), 0); - - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, -1), 0); + final QuadTree tree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 0), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 1), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, -1), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, 0), 0); - testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, 1), 0); + // confirm the root node were added + final AtomicInteger rootNodeCount = new AtomicInteger(0); + tree.forEachRootNode((rootNode) -> { rootNodeCount.addAndGet(1); }); + Assert.assertEquals("incorrect root count", 1, rootNodeCount.get()); + // attempt to get and remove, each node in the tree + final AtomicInteger rootNodePosCount = new AtomicInteger(0); + tree.forEachRootNodePos((renderBufferNode, pos2d) -> + { + DhSectionPos sectionPos = new DhSectionPos(tree.treeMaxDetailLevel, pos2d.x, pos2d.y); + + testGet(tree, sectionPos, 0); + testSet(tree, sectionPos, null); + + rootNodePosCount.addAndGet(1); + }); + Assert.assertEquals("incorrect root count", 1, rootNodeCount.get()); + + } + + @Test + public void OffsetGridListIterationTest() + { + + // offset fully inside (10*0,0) + final QuadTree fullyInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + + DhBlockPos2D fullyInsideOffsetBlockPos = new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS, MINIMUM_TREE_WIDTH_IN_BLOCKS); + fullyInsideTree.setCenterBlockPos(fullyInsideOffsetBlockPos); + + fullyInsideTree.forEachRootNodePos((rootNode, pos2D) -> + { + testSet(fullyInsideTree, new DhSectionPos(fullyInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + }); + + // only 1 root node should be added + final AtomicInteger fullyInsideRootNodeCount = new AtomicInteger(0); + fullyInsideTree.forEachRootNode((rootNode) -> { fullyInsideRootNodeCount.addAndGet(1); }); + Assert.assertEquals("incorrect root count", 1, fullyInsideRootNodeCount.get()); + + + + + // offset fully inside (10*0,0) + final QuadTree borderInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS * 2, MINIMUM_TREE_WIDTH_IN_BLOCKS * 2)); + + borderInsideTree.forEachRootNodePos((rootNode, pos2D) -> + { + testSet(borderInsideTree, new DhSectionPos(borderInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + }); + + // only 1 root node should be added + final AtomicInteger borderInsideRootNodeCount = new AtomicInteger(0); + borderInsideTree.forEachRootNode((rootNode) -> { borderInsideRootNodeCount.addAndGet(1); }); + Assert.assertEquals("incorrect root count", 1, borderInsideRootNodeCount.get()); + + + + + // offset across (10*-1,0) and (10*0,0) + final QuadTree acrossTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + + DhBlockPos2D acrossOffsetBlockPos = new DhBlockPos2D(-MINIMUM_TREE_WIDTH_IN_BLOCKS/4, MINIMUM_TREE_WIDTH_IN_BLOCKS); + acrossTree.setCenterBlockPos(acrossOffsetBlockPos); + + acrossTree.forEachRootNodePos((rootNode, pos2D) -> + { + testSet(acrossTree, new DhSectionPos(acrossTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + }); + + // 2 root nodes should be added + final AtomicInteger acrossRootNodeCount = new AtomicInteger(0); + acrossTree.forEachRootNode((rootNode) -> { acrossRootNodeCount.addAndGet(1); }); + Assert.assertEquals("incorrect root count", 2, acrossRootNodeCount.get()); + + } + + @Test + public void TinyGridAlignedTreeTest() + { + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + // 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()); + + + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 0), 0); + + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, -1), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); + + AtomicInteger rootCount = new AtomicInteger(0); + tree.forEachRootNode((rootNode) -> + { + rootCount.getAndAdd(1); + }); + Assert.assertEquals("incorrect leaf value sum", 1, rootCount.get()); + + } + + @Test + public void TinyGridOffsetTreeTest() + { + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + // 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()); + + + // 2x2 valid positions (overlap the tree's width) + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 0), 0); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, 0), 0); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, -1), 0); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, -1), 0); + + // invalid positions + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, -1, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 1), -1, IndexOutOfBoundsException.class); + + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, 0), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 1, -1), -1, IndexOutOfBoundsException.class); AtomicInteger rootCount = new AtomicInteger(0); @@ -284,7 +430,7 @@ public class QuadTreeTest { rootCount.getAndAdd(1); }); - Assert.assertEquals("incorrect leaf value sum", 9, rootCount.get()); + Assert.assertEquals("incorrect leaf value sum", 4, rootCount.get()); }