Overhaul the QuadTree object
Previously the quad tree was closer to a 3D array than a traditional quadTree. This change brings it closer to a traditional quad tree.
This commit is contained in:
@@ -9,8 +9,7 @@ import com.seibel.lod.core.dependencyInjection.SingletonInjector;
|
||||
import com.seibel.lod.core.generation.tasks.*;
|
||||
import com.seibel.lod.core.pos.*;
|
||||
import com.seibel.lod.core.util.ThreadUtil;
|
||||
import com.seibel.lod.core.util.gridList.MovableGridRingList;
|
||||
import com.seibel.lod.core.util.objects.QuadTree;
|
||||
import com.seibel.lod.core.util.objects.quadTree.QuadTree;
|
||||
import com.seibel.lod.core.util.objects.UncheckedInterruptedException;
|
||||
import com.seibel.lod.core.logging.DhLoggerBuilder;
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
@@ -22,6 +21,7 @@ import java.io.Closeable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WorldGenerationQueue implements Closeable
|
||||
@@ -210,7 +210,7 @@ public class WorldGenerationQueue implements Closeable
|
||||
{
|
||||
AtomicInteger numberOfTasksRemoved = new AtomicInteger();
|
||||
|
||||
this.waitingTaskQuadTree.setCenterPos(targetBlockPos, (worldGenTask) -> { numberOfTasksRemoved.getAndIncrement(); });
|
||||
this.waitingTaskQuadTree.setCenterBlockPos(targetBlockPos, (worldGenTask) -> { numberOfTasksRemoved.getAndIncrement(); });
|
||||
|
||||
// if (numberOfTasksRemoved.get() != 0)
|
||||
// {
|
||||
@@ -218,25 +218,25 @@ public class WorldGenerationQueue implements Closeable
|
||||
// }
|
||||
}
|
||||
|
||||
/** Removes all {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s that have been garbage collected. */
|
||||
private void removeGarbageCollectedTasks() // TODO remove, potential mystery errors caused by garbage collection isn't worth it (and may not be necessary any more now that we are using a quad tree to hold the tasks). // also this is very slow with the curent quad tree impelmentation
|
||||
{
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
{
|
||||
MovableGridRingList<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
Iterator<WorldGenTask> taskIterator = gridRingList.iterator();
|
||||
while (taskIterator.hasNext())
|
||||
{
|
||||
// go through each WorldGenTask in the TaskGroup
|
||||
WorldGenTask genTask = taskIterator.next();
|
||||
if (genTask != null && !genTask.taskTracker.isMemoryAddressValid())
|
||||
{
|
||||
taskIterator.remove();
|
||||
genTask.future.complete(WorldGenResult.CreateFail());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// /** Removes all {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s that have been garbage collected. */
|
||||
// private void removeGarbageCollectedTasks() // TODO remove, potential mystery errors caused by garbage collection isn't worth it (and may not be necessary any more now that we are using a quad tree to hold the tasks). // also this is very slow with the curent quad tree impelmentation
|
||||
// {
|
||||
// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
// {
|
||||
// MovableGridRingList<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
// Iterator<WorldGenTask> taskIterator = gridRingList.iterator();
|
||||
// while (taskIterator.hasNext())
|
||||
// {
|
||||
// // go through each WorldGenTask in the TaskGroup
|
||||
// WorldGenTask genTask = taskIterator.next();
|
||||
// if (genTask != null && !genTask.taskTracker.isMemoryAddressValid())
|
||||
// {
|
||||
// taskIterator.remove();
|
||||
// genTask.future.complete(WorldGenResult.CreateFail());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param targetPos the position to center the generation around
|
||||
@@ -244,53 +244,71 @@ public class WorldGenerationQueue implements Closeable
|
||||
*/
|
||||
private boolean startNextWorldGenTask(DhBlockPos2D targetPos)
|
||||
{
|
||||
WorldGenTask closestTask = null;
|
||||
final AtomicReference<WorldGenTask> closestTaskRef = new AtomicReference<>(null);
|
||||
|
||||
// look through the tree from lowest to highest detail level to find the next task to generate
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
// TODO improve
|
||||
this.waitingTaskQuadTree.forEachRootNode((rootQuadNode) ->
|
||||
{
|
||||
// look for the task that is closest to the targetPos
|
||||
long closestGenDist = Long.MAX_VALUE;
|
||||
|
||||
MovableGridRingList<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
for (WorldGenTask newGenTask : gridRingList)
|
||||
if (closestTaskRef.get() == null)
|
||||
{
|
||||
if (newGenTask != null)
|
||||
rootQuadNode.forAllLeafValues((worldGenTask) ->
|
||||
{
|
||||
if (queueFirstGenerationRequestFound)
|
||||
if (closestTaskRef.get() == null)
|
||||
{
|
||||
// queue the first task we can find
|
||||
closestTask = newGenTask;
|
||||
break;
|
||||
closestTaskRef.set(worldGenTask);
|
||||
}
|
||||
else
|
||||
{
|
||||
// use chebyShev distance in order to generate in rings around the target pos (also because it is a fast distance calculation)
|
||||
int chebDistToTargetPos = newGenTask.pos.getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D());
|
||||
if (chebDistToTargetPos < closestGenDist)
|
||||
{
|
||||
// this task is closer than the last one
|
||||
closestTask = newGenTask;
|
||||
closestGenDist = chebDistToTargetPos;
|
||||
}
|
||||
else if (closestTask != null)
|
||||
{
|
||||
// this task is farther than the last one,
|
||||
// assume we have gotten as close as we can
|
||||
// and queue the task
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// a task has been found, don't look at the next detail level,
|
||||
// everything there will be farther away
|
||||
if (closestTask != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
WorldGenTask closestTask = closestTaskRef.get();
|
||||
|
||||
|
||||
// // look through the tree from lowest to highest detail level to find the next task to generate
|
||||
// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
// {
|
||||
// // look for the task that is closest to the targetPos
|
||||
// long closestGenDist = Long.MAX_VALUE;
|
||||
//
|
||||
// MovableGridRingList<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
// for (WorldGenTask newGenTask : gridRingList)
|
||||
// {
|
||||
// if (newGenTask != null)
|
||||
// {
|
||||
// if (queueFirstGenerationRequestFound)
|
||||
// {
|
||||
// // queue the first task we can find
|
||||
// closestTask = newGenTask;
|
||||
// break;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // use chebyShev distance in order to generate in rings around the target pos (also because it is a fast distance calculation)
|
||||
// int chebDistToTargetPos = newGenTask.pos.getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D());
|
||||
// if (chebDistToTargetPos < closestGenDist)
|
||||
// {
|
||||
// // this task is closer than the last one
|
||||
// closestTask = newGenTask;
|
||||
// closestGenDist = chebDistToTargetPos;
|
||||
// }
|
||||
// else if (closestTask != null)
|
||||
// {
|
||||
// // this task is farther than the last one,
|
||||
// // assume we have gotten as close as we can
|
||||
// // and queue the task
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // a task has been found, don't look at the next detail level,
|
||||
// // everything there will be farther away
|
||||
// if (closestTask != null)
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -303,7 +321,7 @@ public class WorldGenerationQueue implements Closeable
|
||||
|
||||
|
||||
// remove the task we found, we are going to start it and don't want to run it multiple times
|
||||
WorldGenTask removedWorldGenTask = this.waitingTaskQuadTree.set(closestTask.pos.detailLevel, closestTask.pos.x, closestTask.pos.z, null);
|
||||
WorldGenTask removedWorldGenTask = this.waitingTaskQuadTree.set(new DhSectionPos(closestTask.pos.detailLevel, closestTask.pos.x, closestTask.pos.z), null);
|
||||
// removedWorldGenTask can be null // TODO when?
|
||||
|
||||
|
||||
@@ -351,9 +369,9 @@ public class WorldGenerationQueue implements Closeable
|
||||
childFutures.add(newFuture);
|
||||
|
||||
WorldGenTask newGenTask = new WorldGenTask(new DhLodPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ), childDhSectionPos.sectionDetailLevel, removedWorldGenTask.taskTracker, newFuture);
|
||||
this.waitingTaskQuadTree.set(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ, newGenTask);
|
||||
this.waitingTaskQuadTree.set(new DhSectionPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ), newGenTask);
|
||||
|
||||
boolean valueAdded = this.waitingTaskQuadTree.get(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ) != null;
|
||||
boolean valueAdded = this.waitingTaskQuadTree.get(new DhSectionPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ)) != null;
|
||||
LodUtil.assertTrue(valueAdded); // failed to add world gen task to quad tree, this means the quad tree was the wrong size
|
||||
|
||||
// LOGGER.info("split feature "+sectionPos+" into "+childDhSectionPos+" "+(valueAdded ? "added" : "notAdded"));
|
||||
@@ -376,7 +394,7 @@ public class WorldGenerationQueue implements Closeable
|
||||
LodUtil.assertTrue(taskDetailLevel >= this.minDataDetail && taskDetailLevel <= this.maxDataDetail);
|
||||
|
||||
DhChunkPos chunkPosMin = new DhChunkPos(taskPos.getCornerBlockPos());
|
||||
// LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin);
|
||||
LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin);
|
||||
|
||||
this.numberOfTasksQueued++;
|
||||
inProgressTaskGroup.genFuture = startGenerationEvent(this.generator, chunkPosMin, granularity, taskDetailLevel, inProgressTaskGroup.group::onGenerationComplete);
|
||||
@@ -414,22 +432,22 @@ public class WorldGenerationQueue implements Closeable
|
||||
queueingThread.shutdownNow();
|
||||
|
||||
// remove any incomplete generation tasks
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
{
|
||||
MovableGridRingList<WorldGenTask> ringList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
ringList.clear((worldGenTask) ->
|
||||
{
|
||||
if (worldGenTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
worldGenTask.future.cancel(true);
|
||||
}
|
||||
catch (CancellationException ignored)
|
||||
{ /* don't log shutdown exceptions */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
// {
|
||||
// MovableGridRingList<WorldGenTask> ringList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
// ringList.clear((worldGenTask) ->
|
||||
// {
|
||||
// if (worldGenTask != null)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// worldGenTask.future.cancel(true);
|
||||
// }
|
||||
// catch (CancellationException ignored)
|
||||
// { /* don't log shutdown exceptions */ }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// stop and remove any in progress tasks
|
||||
|
||||
@@ -28,6 +28,8 @@ public class DhLodPos implements Comparable<DhLodPos>
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
}
|
||||
public DhLodPos(DhSectionPos sectionPos) { this(sectionPos.sectionDetailLevel, sectionPos.sectionX, sectionPos.sectionZ); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ public class DhSectionPos
|
||||
this.sectionZ = sectionZ;
|
||||
}
|
||||
|
||||
public DhSectionPos(DhBlockPos blockPos)
|
||||
public DhSectionPos(DhBlockPos blockPos) { this(new DhBlockPos2D(blockPos)); }
|
||||
public DhSectionPos(DhBlockPos2D blockPos)
|
||||
{
|
||||
DhLodPos lodPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPos.x, blockPos.z);
|
||||
lodPos = lodPos.convertToDetailLevel(SECTION_BLOCK_DETAIL_LEVEL);
|
||||
@@ -76,7 +77,7 @@ public class DhSectionPos
|
||||
|
||||
|
||||
/** Returns the center for the highest detail level (0) */
|
||||
public DhLodPos getCenter() { return this.getCenter((byte) 0); }
|
||||
public DhLodPos getCenter() { return this.getCenter((byte) 0); } // TODO why does this use detail level 0 instead of this object's detail level?
|
||||
public DhLodPos getCenter(byte returnDetailLevel)
|
||||
{
|
||||
LodUtil.assertTrue(returnDetailLevel <= this.sectionDetailLevel, "returnDetailLevel must be less than sectionDetail");
|
||||
@@ -110,6 +111,15 @@ 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
|
||||
public DhSectionPos convertToDetailLevel(byte newSectionDetailLevel)
|
||||
{
|
||||
DhLodPos lodPos = new DhLodPos(this.sectionDetailLevel, this.sectionX, this.sectionZ);
|
||||
lodPos = lodPos.convertToDetailLevel(newSectionDetailLevel);
|
||||
|
||||
DhSectionPos newPos = new DhSectionPos(newSectionDetailLevel, lodPos);
|
||||
return newPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DhLodPos 1 detail level lower <br><br>
|
||||
@@ -159,6 +169,18 @@ public class DhSectionPos
|
||||
/** NOTE: This does not consider yOffset! */
|
||||
public boolean overlaps(DhSectionPos other) { return this.getSectionBBoxPos().overlapsExactly(other.getSectionBBoxPos()); }
|
||||
|
||||
/** NOTE: This does not consider yOffset! */
|
||||
public boolean contains(DhSectionPos otherPos)
|
||||
{
|
||||
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());
|
||||
|
||||
return thisMinBlockPos.x <= otherCornerBlockPos.x && otherCornerBlockPos.x <= thisMaxBlockPos.x &&
|
||||
thisMinBlockPos.z <= otherCornerBlockPos.z && otherCornerBlockPos.z <= thisMaxBlockPos.z;
|
||||
}
|
||||
|
||||
/** Serialize() is different from toString() as it must NEVER be changed, and should be in a short format */
|
||||
public String serialize() { return "[" + this.sectionDetailLevel + ',' + this.sectionX + ',' + this.sectionZ + ']'; }
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public class RenderBufferHandler
|
||||
|
||||
MovableGridRingList<LodRenderSection> referenceList = quadTree.getRingListForDetailLevel((byte) (quadTree.getNumbersOfSectionDetailLevels() - 1));
|
||||
Pos2D center = referenceList.getCenter();
|
||||
this.renderBufferNodesGridList = new MovableGridRingList<>(referenceList.getHalfSize(), center);
|
||||
this.renderBufferNodesGridList = new MovableGridRingList<>(referenceList.getHalfWidth(), center);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
private final AtomicReference<Pos2D> minPosRef = new AtomicReference<>();
|
||||
|
||||
/** width of this grid list */
|
||||
private final int size;
|
||||
private final int halfSize;
|
||||
private final int width;
|
||||
/** radius or half-width of this grid list */
|
||||
private final int halfWidth;
|
||||
|
||||
private final ReentrantReadWriteLock moveLock = new ReentrantReadWriteLock();
|
||||
|
||||
@@ -48,14 +49,14 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
// constructors //
|
||||
//==============//
|
||||
|
||||
public MovableGridRingList(int halfSize, Pos2D center) { this(halfSize, center.x, center.y); }
|
||||
public MovableGridRingList(int halfSize, int centerX, int centerY)
|
||||
public MovableGridRingList(int halfWidth, Pos2D center) { this(halfWidth, center.x, center.y); }
|
||||
public MovableGridRingList(int halfWidth, int centerX, int centerY)
|
||||
{
|
||||
super((halfSize * 2 + 1) * (halfSize * 2 + 1));
|
||||
super((halfWidth * 2 + 1) * (halfWidth * 2 + 1));
|
||||
|
||||
this.size = halfSize * 2 + 1;
|
||||
this.halfSize = halfSize;
|
||||
this.minPosRef.set(new Pos2D(centerX-halfSize, centerY-halfSize));
|
||||
this.width = halfWidth * 2 + 1;
|
||||
this.halfWidth = halfWidth;
|
||||
this.minPosRef.set(new Pos2D(centerX- halfWidth, centerY- halfWidth));
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@@ -204,9 +205,9 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
}
|
||||
|
||||
super.clear();
|
||||
super.ensureCapacity(this.size * this.size);
|
||||
super.ensureCapacity(this.width * this.width);
|
||||
// TODO why are we filling the array will nulls? everything should already be null after the clear
|
||||
for (int i = 0; i < this.size * this.size; i++)
|
||||
for (int i = 0; i < this.width * this.width; i++)
|
||||
{
|
||||
super.add(null);
|
||||
}
|
||||
@@ -227,8 +228,8 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
public boolean moveTo(int newCenterX, int newCenterY, Consumer<? super T> removedItemConsumer, BiConsumer<Pos2D, ? super T> nullableRemovedItemConsumer)
|
||||
{
|
||||
Pos2D cPos = this.minPosRef.get();
|
||||
int newMinX = newCenterX - this.halfSize;
|
||||
int newMinY = newCenterY - this.halfSize;
|
||||
int newMinX = newCenterX - this.halfWidth;
|
||||
int newMinY = newCenterY - this.halfWidth;
|
||||
if (cPos.x == newMinX && cPos.y == newMinY)
|
||||
{
|
||||
return false;
|
||||
@@ -248,22 +249,22 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
// if the x or z offset is equal to or greater than
|
||||
// the total width, just delete the current data
|
||||
// and update the pos
|
||||
if (Math.abs(deltaX) >= this.size || Math.abs(deltaY) >= this.size)
|
||||
if (Math.abs(deltaX) >= this.width || Math.abs(deltaY) >= this.width)
|
||||
{
|
||||
this.clear(removedItemConsumer);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int x = 0; x < this.size; x++)
|
||||
for (int x = 0; x < this.width; x++)
|
||||
{
|
||||
for (int y = 0; y < this.size; y++)
|
||||
for (int y = 0; y < this.width; y++)
|
||||
{
|
||||
Pos2D itemPos = new Pos2D(x+cPos.x, y+cPos.y);
|
||||
|
||||
if (x - deltaX < 0
|
||||
|| y - deltaY < 0
|
||||
|| x - deltaX >= this.size
|
||||
|| y - deltaY >= this.size)
|
||||
|| x - deltaX >= this.width
|
||||
|| y - deltaY >= this.width)
|
||||
{
|
||||
T item = this._swapUnsafe(itemPos.x, itemPos.y, null);
|
||||
if (item != null && removedItemConsumer != null)
|
||||
@@ -299,13 +300,13 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
// position getters //
|
||||
//==================//
|
||||
|
||||
public Pos2D getCenter() { return new Pos2D(this.minPosRef.get().x + this.halfSize, this.minPosRef.get().y + this.halfSize); }
|
||||
public Pos2D getCenter() { return new Pos2D(this.minPosRef.get().x + this.halfWidth, this.minPosRef.get().y + this.halfWidth); }
|
||||
|
||||
public Pos2D getMinPosInRange() { return this.minPosRef.get(); }
|
||||
public Pos2D getMaxPosInRange() { return new Pos2D(this.minPosRef.get().x + this.size-1, this.minPosRef.get().y + this.size-1); }
|
||||
public Pos2D getMaxPosInRange() { return new Pos2D(this.minPosRef.get().x + this.width -1, this.minPosRef.get().y + this.width -1); }
|
||||
|
||||
public int getSize() { return this.size; }
|
||||
public int getHalfSize() { return this.halfSize; }
|
||||
public int getWidth() { return this.width; }
|
||||
public int getHalfWidth() { return this.halfWidth; }
|
||||
|
||||
|
||||
|
||||
@@ -321,22 +322,22 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
{
|
||||
Pos2D minPos = this.minPosRef.get();
|
||||
return (x>=minPos.x
|
||||
&& x<minPos.x+this.size
|
||||
&& x<minPos.x+this.width
|
||||
&& y>=minPos.y
|
||||
&& y<minPos.y+this.size);
|
||||
&& y<minPos.y+this.width);
|
||||
}
|
||||
|
||||
private boolean _inRangeAcquired(int x, int y, Pos2D min)
|
||||
{
|
||||
return (x>=min.x
|
||||
&& x<min.x+this.size
|
||||
&& x<min.x+this.width
|
||||
&& y>=min.y
|
||||
&& y<min.y+this.size);
|
||||
&& y<min.y+this.width);
|
||||
}
|
||||
|
||||
private T _getUnsafe(int x, int y) { return super.get(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size); }
|
||||
private void _setUnsafe(int x, int y, T item) { super.set(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size, item); }
|
||||
private T _swapUnsafe(int x, int y, T item) { return super.set(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size, item); }
|
||||
private T _getUnsafe(int x, int y) { return super.get(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width); }
|
||||
private void _setUnsafe(int x, int y, T item) { super.set(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width, item); }
|
||||
private T _swapUnsafe(int x, int y, T item) { return super.set(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width, item); }
|
||||
|
||||
|
||||
// TODO: implement this
|
||||
@@ -378,9 +379,9 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
try
|
||||
{
|
||||
Pos2D min = this.minPosRef.get();
|
||||
for (int x = min.x; x < min.x + this.size; x++)
|
||||
for (int x = min.x; x < min.x + this.width; x++)
|
||||
{
|
||||
for (int y = min.y; y < min.y + this.size; y++)
|
||||
for (int y = min.y; y < min.y + this.width; y++)
|
||||
{
|
||||
T t = this._getUnsafe(x, y);
|
||||
consumer.accept(t, new Pos2D(x, y));
|
||||
@@ -463,12 +464,12 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
private void createRingIteratorList()
|
||||
{
|
||||
this.ringPositionIteratorArray = null;
|
||||
Pos2D[] posArray = new Pos2D[this.size*this.size];
|
||||
Pos2D[] posArray = new Pos2D[this.width *this.width];
|
||||
|
||||
int i = 0;
|
||||
for (int xPos = -this.halfSize; xPos <= this.halfSize; xPos++)
|
||||
for (int xPos = -this.halfWidth; xPos <= this.halfWidth; xPos++)
|
||||
{
|
||||
for (int zPos = -this.halfSize; zPos <= this.halfSize; zPos++)
|
||||
for (int zPos = -this.halfWidth; zPos <= this.halfWidth; zPos++)
|
||||
{
|
||||
posArray[i] = new Pos2D(xPos, zPos);
|
||||
i++;
|
||||
@@ -485,12 +486,12 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
|
||||
for (int j = 0; j < posArray.length; j++)
|
||||
{
|
||||
posArray[j] = posArray[j].add(new Pos2D(this.halfSize, this.halfSize));
|
||||
posArray[j] = posArray[j].add(new Pos2D(this.halfWidth, this.halfWidth));
|
||||
}
|
||||
for (Pos2D pos2D : posArray)
|
||||
{
|
||||
LodUtil.assertTrue(pos2D.x >= 0 && pos2D.x < this.size);
|
||||
LodUtil.assertTrue(pos2D.y >= 0 && pos2D.y < this.size);
|
||||
LodUtil.assertTrue(pos2D.x >= 0 && pos2D.x < this.width);
|
||||
LodUtil.assertTrue(pos2D.y >= 0 && pos2D.y < this.width);
|
||||
}
|
||||
|
||||
this.ringPositionIteratorArray = posArray;
|
||||
@@ -507,7 +508,7 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
public String toString()
|
||||
{
|
||||
Pos2D p = this.minPosRef.get();
|
||||
return this.getClass().getSimpleName() + "[" + (p.x+this.halfSize) + "," + (p.y+this.halfSize) + "] " + this.size + "*" + this.size + "[" + this.size() + "]";
|
||||
return this.getClass().getSimpleName() + "[" + (p.x+this.halfWidth) + "," + (p.y+this.halfWidth) + "] " + this.width + "*" + this.width + "[" + this.size() + "]";
|
||||
}
|
||||
|
||||
public String toDetailString()
|
||||
@@ -522,7 +523,7 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
|
||||
str.append(t != null ? t.toString() : "NULL");
|
||||
str.append(", ");
|
||||
i++;
|
||||
if (i % this.size == 0)
|
||||
if (i % this.width == 0)
|
||||
{
|
||||
str.append("\n");
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
package com.seibel.lod.core.util.objects;
|
||||
|
||||
import com.seibel.lod.core.dataObjects.render.ColumnRenderSource;
|
||||
import com.seibel.lod.core.logging.DhLoggerBuilder;
|
||||
import com.seibel.lod.core.pos.DhBlockPos2D;
|
||||
import com.seibel.lod.core.pos.DhSectionPos;
|
||||
import com.seibel.lod.core.pos.Pos2D;
|
||||
import com.seibel.lod.core.render.LodQuadTree;
|
||||
import com.seibel.lod.core.util.BitShiftUtil;
|
||||
import com.seibel.lod.core.util.DetailDistanceUtil;
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
import com.seibel.lod.core.util.gridList.MovableGridRingList;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This class represents a quadTree of T type values.
|
||||
*/
|
||||
public class QuadTree<T>
|
||||
{
|
||||
/**
|
||||
* Note: all config values should be via the class that extends this class, and
|
||||
* by implementing different abstract methods
|
||||
*/
|
||||
public static final byte TREE_LOWEST_DETAIL_LEVEL = ColumnRenderSource.SECTION_SIZE_OFFSET;
|
||||
|
||||
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
|
||||
|
||||
|
||||
public final byte getLayerDetailLevelOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; }
|
||||
public final byte getLayerDetailLevel(byte sectionDetailLevel) { return (byte) (sectionDetailLevel - this.getLayerDetailLevelOffset()); }
|
||||
|
||||
public final byte getLayerSectionDetailOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; }
|
||||
public final byte getLayerSectionDetail(byte dataDetail) { return (byte) (dataDetail + this.getLayerSectionDetailOffset()); }
|
||||
|
||||
|
||||
/** AKA how many detail levels are in this quad tree */
|
||||
public final byte numbersOfSectionDetailLevels;
|
||||
/** related to {@link QuadTree#numbersOfSectionDetailLevels}, the largest number detail level in this tree. */
|
||||
public final byte treeMaxDetailLevel;
|
||||
|
||||
/** contain the actual data in the quad tree structure */
|
||||
private final MovableGridRingList<T>[] ringLists;
|
||||
|
||||
public final int blockRenderDistance;
|
||||
|
||||
DhBlockPos2D centerBlockPos;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor of the quadTree
|
||||
* @param viewDistance View distance in blocks
|
||||
*/
|
||||
public QuadTree(
|
||||
int viewDistance,
|
||||
DhBlockPos2D centerBlockPos)
|
||||
{
|
||||
DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else
|
||||
this.blockRenderDistance = viewDistance;
|
||||
this.centerBlockPos = centerBlockPos;
|
||||
|
||||
|
||||
// Calculate the max section detail level //
|
||||
|
||||
byte maxDetailLevel = this.getMaxDetailLevelInRange(viewDistance * Math.sqrt(2));
|
||||
this.treeMaxDetailLevel = this.getLayerSectionDetail(maxDetailLevel);
|
||||
this.numbersOfSectionDetailLevels = (byte) (this.treeMaxDetailLevel + 1);
|
||||
this.ringLists = new MovableGridRingList[this.numbersOfSectionDetailLevels - TREE_LOWEST_DETAIL_LEVEL];
|
||||
|
||||
|
||||
|
||||
// Construct the ringLists //
|
||||
|
||||
LOGGER.info("Creating "+MovableGridRingList.class.getSimpleName()+" with player center at "+this.centerBlockPos);
|
||||
for (byte sectionDetailLevel = TREE_LOWEST_DETAIL_LEVEL; sectionDetailLevel < this.numbersOfSectionDetailLevels; sectionDetailLevel++)
|
||||
{
|
||||
byte targetDetailLevel = this.getLayerDetailLevel(sectionDetailLevel);
|
||||
int maxDist = this.getFurthestBlockDistanceForDetailLevel(targetDetailLevel);
|
||||
|
||||
// TODO temp fix that may or may not allocate the right amount, but it works well enough for now
|
||||
// int halfSize = MathUtil.ceilDiv(maxDist, BitShiftUtil.powerOfTwo(sectionDetailLevel)) + 8; // +8 to make sure the section is fully contained in the ringList //TODO what does the "8" represent?
|
||||
int halfSize = BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel-targetDetailLevel); //MathUtil.ceilDiv(maxDist, sectionDetailLevel) + 8; // +8 to make sure the section is fully contained in the ringList //TODO what does the "8" represent?
|
||||
|
||||
// check that the detail level and position are valid
|
||||
DhSectionPos checkedPos = new DhSectionPos(sectionDetailLevel, halfSize, halfSize);
|
||||
byte checkedDetailLevel = this.calculateExpectedDetailLevel(this.centerBlockPos, checkedPos);
|
||||
// validate the detail level
|
||||
LodUtil.assertTrue(checkedDetailLevel > targetDetailLevel,
|
||||
"in "+sectionDetailLevel+", getFurthestDistance would return "+maxDist+" which would be contained in range "+(halfSize-2)+", but calculateExpectedDetailLevel at "+checkedPos+" is "+checkedDetailLevel+" <= "+targetDetailLevel);
|
||||
|
||||
|
||||
// create the new ring list
|
||||
Pos2D ringListCenterPos = new Pos2D(BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, sectionDetailLevel), BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, sectionDetailLevel));
|
||||
|
||||
LOGGER.info("Creating "+MovableGridRingList.class.getSimpleName()+" centered on "+ringListCenterPos+" with halfSize ["+halfSize+"] (maxDist ["+maxDist+"], dataDetail ["+targetDetailLevel+"])");
|
||||
this.ringLists[sectionDetailLevel - TREE_LOWEST_DETAIL_LEVEL] = new MovableGridRingList<>(halfSize, ringListCenterPos.x, ringListCenterPos.y);
|
||||
|
||||
}
|
||||
|
||||
}// constructor
|
||||
|
||||
|
||||
|
||||
//=====================//
|
||||
// getters and setters //
|
||||
//=====================//
|
||||
|
||||
/** @return the value at the given section position */
|
||||
public final T get(DhSectionPos pos) { return this.get(pos.sectionDetailLevel, pos.sectionX, pos.sectionZ); }
|
||||
/**
|
||||
* @param detailLevel detail level of the section
|
||||
* @param x x coordinate of the section
|
||||
* @param z z coordinate of the section
|
||||
* @return the value for the given section position
|
||||
*/
|
||||
public final T get(byte detailLevel, int x, int z) { return this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL].get(x, z); }
|
||||
|
||||
|
||||
/** @return the value that was previously in the given position, null if nothing */
|
||||
public final T set(DhSectionPos pos, T value) { return this.set(pos.sectionDetailLevel, pos.sectionX, pos.sectionZ, value); }
|
||||
/** @return the value that was previously in the given position, null if nothing */
|
||||
public final T set(byte detailLevel, int x, int z, T value)
|
||||
{
|
||||
T previousValue = this.get(detailLevel, x, z);
|
||||
this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL].set(x, z, value);
|
||||
return previousValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//===============//
|
||||
// raw ringLists //
|
||||
//===============//
|
||||
|
||||
/**
|
||||
* This method returns the RingList for the given detail level
|
||||
* @apiNote The returned ringList should not be modified! <br> TODO why? could it cause concurrent modification exceptions? is this only the case for {@link LodQuadTree}?
|
||||
* @param detailLevel the detail level
|
||||
* @return the RingList
|
||||
*/
|
||||
public final MovableGridRingList<T> getRingList(byte detailLevel) { return this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL]; }
|
||||
|
||||
public Iterator<T> getRingListIterator(byte detailLevel) { return this.getRingList(detailLevel).iterator(); }
|
||||
|
||||
|
||||
|
||||
//================//
|
||||
// get/set center //
|
||||
//================//
|
||||
|
||||
public void setCenterPos(DhBlockPos2D newCenterPos) { this.setCenterPos(newCenterPos, null); }
|
||||
public void setCenterPos(DhBlockPos2D newCenterPos, Consumer<? super T> removedItemConsumer)
|
||||
{
|
||||
this.centerBlockPos = newCenterPos;
|
||||
|
||||
// recenter the grid lists if necessary
|
||||
for (int sectionDetailLevel = TREE_LOWEST_DETAIL_LEVEL; sectionDetailLevel < this.numbersOfSectionDetailLevels; sectionDetailLevel++)
|
||||
{
|
||||
Pos2D expectedCenterPos = new Pos2D(BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, sectionDetailLevel), BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, sectionDetailLevel));
|
||||
MovableGridRingList<T> gridList = this.ringLists[sectionDetailLevel - TREE_LOWEST_DETAIL_LEVEL];
|
||||
|
||||
if (!gridList.getCenter().equals(expectedCenterPos))
|
||||
{
|
||||
gridList.moveTo(expectedCenterPos.x, expectedCenterPos.y, removedItemConsumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final DhBlockPos2D getCenterPos() { return this.centerBlockPos; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//===========================//
|
||||
// detail level calculations //
|
||||
//===========================//
|
||||
|
||||
/**
|
||||
* This method will compute the detail level based on target position and section pos.
|
||||
* @param targetPos can be the player's position. A reference for calculating the detail level
|
||||
* @return detail level of this section pos
|
||||
*/
|
||||
public final byte calculateExpectedDetailLevel(DhBlockPos2D targetPos, DhSectionPos sectionPos)
|
||||
{
|
||||
return DetailDistanceUtil.getDetailLevelFromDistance(
|
||||
targetPos.dist(sectionPos.getCenter().getCenterBlockPos()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest detail level in a circle around the center.<br>
|
||||
* Note: the returned distance should always be the ceiling estimation of the circleRadius.
|
||||
* @return the highest detail level in the circle
|
||||
*/
|
||||
public final byte getMaxDetailLevelInRange(double circleRadius) { return DetailDistanceUtil.getDetailLevelFromDistance(circleRadius); }
|
||||
|
||||
/**
|
||||
* Returns the furthest distance to the center for the given detail level. <br>
|
||||
* Note: the returned distance should always be the ceiling estimation of the circleRadius.
|
||||
* @return the furthest distance to the center, in blocks
|
||||
*/
|
||||
public final int getFurthestBlockDistanceForDetailLevel(byte detailLevl)
|
||||
{
|
||||
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevl + 1));
|
||||
// +1 because that's the border to the next detail level, and we want to include up to it.
|
||||
}
|
||||
|
||||
/** Given a section pos at level n this method returns the parent value at level n+1 */
|
||||
public final T getParentValue(DhSectionPos pos) { return this.get(pos.getParentPos()); }
|
||||
|
||||
/**
|
||||
* Given a section pos at level n and a child index, this returns the child section at level n-1
|
||||
* @param child0to3 since there are 4 possible children this index identifies which one we are getting
|
||||
*/
|
||||
public final T getChildValue(DhSectionPos pos, int child0to3) { return this.get(pos.getChildByIndex(child0to3)); }
|
||||
|
||||
|
||||
|
||||
//==============//
|
||||
// base methods //
|
||||
//==============//
|
||||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.treeMaxDetailLevel; detailLevel++)
|
||||
{
|
||||
if (!isDetailLevelEmpty(detailLevel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public boolean isDetailLevelEmpty(byte detailLevel) { return this.getRingList(detailLevel).isEmpty(); }
|
||||
|
||||
/** returns the number of items in this QuadTree */
|
||||
public int size()
|
||||
{
|
||||
int size = 0;
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.treeMaxDetailLevel; detailLevel++)
|
||||
{
|
||||
size += getRingList(detailLevel).size();
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
|
||||
public String getDebugString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte i = 0; i < this.ringLists.length; i++)
|
||||
{
|
||||
sb.append("Layer ").append(i + TREE_LOWEST_DETAIL_LEVEL).append(":\n");
|
||||
sb.append(this.ringLists[i].toDetailString());
|
||||
sb.append("\n");
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
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 org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class QuadNode<T>
|
||||
{
|
||||
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
|
||||
|
||||
|
||||
public DhSectionPos sectionPos;
|
||||
public T value;
|
||||
|
||||
|
||||
/**
|
||||
* North West <br>
|
||||
* index 0 <br>
|
||||
* relative pos (0,0)
|
||||
*/
|
||||
public QuadNode<T> nwChild;
|
||||
/**
|
||||
* North East <br>
|
||||
* index 1 <br>
|
||||
* relative (1,0)
|
||||
*/
|
||||
public QuadNode<T> neChild;
|
||||
/**
|
||||
* South West <br>
|
||||
* index 2 <br>
|
||||
* relative (0,1)
|
||||
*/
|
||||
public QuadNode<T> swChild;
|
||||
/**
|
||||
* South East <br>
|
||||
* index 3 <br>
|
||||
* relative (1,1)
|
||||
*/
|
||||
public QuadNode<T> seChild;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public QuadNode(DhSectionPos sectionPos)
|
||||
{
|
||||
this.sectionPos = sectionPos;
|
||||
}
|
||||
|
||||
|
||||
/** @return the number of non-null child nodes */
|
||||
public int childCount()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (this.getChildByIndex(i) != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the DhLodPos 1 detail level lower <br><br>
|
||||
*
|
||||
* Relative child positions returned for each index: <br>
|
||||
* 0 = (0,0) <br>
|
||||
* 1 = (1,0) <br>
|
||||
* 2 = (0,1) <br>
|
||||
* 3 = (1,1) <br>
|
||||
*
|
||||
* @param child0to3 must be an int between 0 and 3
|
||||
*/
|
||||
public QuadNode<T> getChildByIndex(int child0to3) throws IllegalArgumentException
|
||||
{
|
||||
switch (child0to3)
|
||||
{
|
||||
case 0:
|
||||
return nwChild;
|
||||
case 1:
|
||||
return neChild;
|
||||
case 2:
|
||||
return swChild;
|
||||
case 3:
|
||||
return seChild;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("child0to3 must be between 0 and 3");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param sectionPos must be 1 detail level lower than this node's detail level
|
||||
* @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this node
|
||||
* @return the node at the given position
|
||||
*/
|
||||
public T getValue(DhSectionPos sectionPos) throws IllegalArgumentException { return this.getOrSetValue(sectionPos, false, null); }
|
||||
/**
|
||||
* @param sectionPos must be 1 detail level lower than this node's detail level
|
||||
* @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this node
|
||||
* @return the node at the given position before the new node was set
|
||||
*/
|
||||
public T setValue(DhSectionPos sectionPos, T newValue) throws IllegalArgumentException { return this.getOrSetValue(sectionPos, true, newValue); }
|
||||
/**
|
||||
* @param inputSectionPos must be 1 detail level lower than this node's detail level
|
||||
* @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this
|
||||
* @return the node at the given position before the new node was set (if the new node should be set)
|
||||
*/
|
||||
private T getOrSetValue(DhSectionPos inputSectionPos, boolean replaceValue, T newValue) throws IllegalArgumentException
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
if (inputSectionPos.sectionDetailLevel > this.sectionPos.sectionDetailLevel)
|
||||
{
|
||||
throw new IllegalArgumentException("detail level higher than this node. Node Detail level: "+this.sectionPos.sectionDetailLevel+" input detail level: "+inputSectionPos.sectionDetailLevel);
|
||||
}
|
||||
|
||||
if (inputSectionPos.sectionDetailLevel == this.sectionPos.sectionDetailLevel && !inputSectionPos.equals(this.sectionPos))
|
||||
{
|
||||
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.sectionPos.sectionDetailLevel)
|
||||
{
|
||||
// this node is the requested position
|
||||
T returnValue = this.value;
|
||||
if (replaceValue)
|
||||
{
|
||||
this.value = newValue;
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// this node is a parent to the position requested,
|
||||
// recurse to the next node
|
||||
|
||||
// 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();
|
||||
|
||||
// may or may not be at the requested detail level
|
||||
QuadNode<T> childNode;
|
||||
if (inputCenterPos.x <= nodeCenterPos.x)
|
||||
{
|
||||
if (inputCenterPos.z <= nodeCenterPos.z)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (replaceValue && this.neChild == null)
|
||||
{
|
||||
this.neChild = new QuadNode<>(this.sectionPos.getChildByIndex(2));
|
||||
}
|
||||
// LOGGER.info("NE");
|
||||
childNode = this.neChild;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
callback.accept(this.getChildByIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given consumer to all leaf nodes below this node. <br>
|
||||
* Note: this will pass in null values.
|
||||
*/
|
||||
public void forAllLeafValues(Consumer<? super T> callback)
|
||||
{
|
||||
if (this.childCount() == 0)
|
||||
{
|
||||
// base case, bottom leaf node found
|
||||
callback.accept(this.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
QuadNode<T> childNode = this.getChildByIndex(i);
|
||||
if (childNode != null)
|
||||
{
|
||||
// TODO should this pass in a null value if the child node is null?
|
||||
childNode.forAllLeafValues(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() { return "pos: "+this.sectionPos+", value: "+this.value; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
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.DhSectionPos;
|
||||
import com.seibel.lod.core.pos.Pos2D;
|
||||
import com.seibel.lod.core.util.BitShiftUtil;
|
||||
import com.seibel.lod.core.util.DetailDistanceUtil;
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
import com.seibel.lod.core.util.gridList.MovableGridRingList;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This class represents a quadTree of T type values.
|
||||
*/
|
||||
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;
|
||||
|
||||
/** contain the actual data in the quad tree structure */
|
||||
private final MovableGridRingList<QuadNode<T>> topRingList;
|
||||
|
||||
DhBlockPos2D centerBlockPos;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor of the quadTree
|
||||
*/
|
||||
public QuadTree(
|
||||
int viewDistanceInBlocks,
|
||||
DhBlockPos2D centerBlockPos)
|
||||
{
|
||||
DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else
|
||||
this.centerBlockPos = centerBlockPos;
|
||||
|
||||
this.treeMaxDetailLevel = 10; // TODO 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(viewDistanceInBlocks, 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
|
||||
|
||||
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);
|
||||
|
||||
}// constructor
|
||||
|
||||
|
||||
|
||||
//=====================//
|
||||
// getters and setters //
|
||||
//=====================//
|
||||
|
||||
/** @return the value at the given section position */
|
||||
public final T get(DhSectionPos pos) throws IndexOutOfBoundsException { return this.getOrSet(pos, false, null); }
|
||||
/** @return the value that was previously in the given position, null if nothing */
|
||||
public final T set(DhSectionPos pos, T value) throws IndexOutOfBoundsException { return this.getOrSet(pos, true, value); }
|
||||
|
||||
protected final T getOrSet(DhSectionPos pos, boolean setNewValue, T newValue) throws IndexOutOfBoundsException
|
||||
{
|
||||
if (this.isPositionInBounds(pos))
|
||||
{
|
||||
DhSectionPos rootPos = pos.convertToDetailLevel(this.treeMaxDetailLevel);
|
||||
int ringListPosX = rootPos.sectionX;
|
||||
int ringListPosZ = rootPos.sectionZ;
|
||||
|
||||
QuadNode<T> topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ);
|
||||
if (topQuadNode == null)
|
||||
{
|
||||
topQuadNode = new QuadNode<T>(rootPos);
|
||||
boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode);
|
||||
LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: "+rootPos);
|
||||
}
|
||||
|
||||
if (!topQuadNode.sectionPos.contains(pos))
|
||||
{
|
||||
LodUtil.assertNotReach("failed to get a root node that contains the input position: "+pos+" root node pos: "+topQuadNode.sectionPos);
|
||||
}
|
||||
|
||||
|
||||
T returnValue = topQuadNode.getValue(pos);
|
||||
if (setNewValue)
|
||||
{
|
||||
topQuadNode.setValue(pos, newValue);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO give the min and max allowed positions
|
||||
throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, given Position: "+pos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isPositionInBounds(DhSectionPos pos)
|
||||
{
|
||||
DhSectionPos blockPos = pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL);
|
||||
|
||||
int halfWidthInBlocks = BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel) * Math.floorDiv(this.topRingList.getWidth(), 2);
|
||||
|
||||
int minX = this.centerBlockPos.x - halfWidthInBlocks;
|
||||
int maxX = this.centerBlockPos.x + halfWidthInBlocks;
|
||||
|
||||
int minZ = this.centerBlockPos.z - halfWidthInBlocks;
|
||||
int maxZ = this.centerBlockPos.z + halfWidthInBlocks;
|
||||
|
||||
return minX <= blockPos.sectionX && blockPos.sectionX < maxX &&
|
||||
minZ <= blockPos.sectionZ && blockPos.sectionZ < maxZ;
|
||||
}
|
||||
|
||||
|
||||
/** no nulls TODO */
|
||||
public void forEachRootNode(Consumer<QuadNode<T>> consumer)
|
||||
{
|
||||
this.topRingList.forEachOrdered((rootNode) ->
|
||||
{
|
||||
if (rootNode != null)
|
||||
{
|
||||
consumer.accept(rootNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void forEachLeafValue(Consumer<? super T> consumer)
|
||||
{
|
||||
this.forEachRootNode((rootNode) ->
|
||||
{
|
||||
rootNode.forAllLeafValues(consumer);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//================//
|
||||
// get/set center //
|
||||
//================//
|
||||
|
||||
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); }
|
||||
public void setCenterBlockPos(DhBlockPos2D newCenterPos, Consumer<QuadNode<? super T>> removedItemConsumer)
|
||||
{
|
||||
this.centerBlockPos = newCenterPos;
|
||||
|
||||
Pos2D expectedCenterPos = new Pos2D(
|
||||
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMaxDetailLevel),
|
||||
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMaxDetailLevel));
|
||||
|
||||
if (!this.topRingList.getCenter().equals(expectedCenterPos))
|
||||
{
|
||||
this.topRingList.moveTo(expectedCenterPos.x, expectedCenterPos.y, removedItemConsumer);
|
||||
}
|
||||
}
|
||||
|
||||
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
|
||||
|
||||
|
||||
|
||||
//===========================//
|
||||
// detail level calculations //
|
||||
//===========================//
|
||||
|
||||
/**
|
||||
* This method will compute the detail level based on target position and section pos.
|
||||
* @param targetPos can be the player's position. A reference for calculating the detail level
|
||||
* @return detail level of this section pos
|
||||
*/
|
||||
public final byte calculateExpectedDetailLevel(DhBlockPos2D targetPos, DhSectionPos sectionPos)
|
||||
{
|
||||
return DetailDistanceUtil.getDetailLevelFromDistance(
|
||||
targetPos.dist(sectionPos.getCenter().getCenterBlockPos()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest detail level in a circle around the center.<br>
|
||||
* Note: the returned distance should always be the ceiling estimation of the circleRadius.
|
||||
* @return the highest detail level in the circle
|
||||
*/
|
||||
public final byte getMaxDetailLevelInRange(double circleRadius) { return DetailDistanceUtil.getDetailLevelFromDistance(circleRadius); }
|
||||
|
||||
/**
|
||||
* Returns the furthest distance to the center for the given detail level. <br>
|
||||
* Note: the returned distance should always be the ceiling estimation of the circleRadius.
|
||||
* @return the furthest distance to the center, in blocks
|
||||
*/
|
||||
public final int getFurthestBlockDistanceForDetailLevel(byte detailLevl)
|
||||
{
|
||||
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevl + 1));
|
||||
// +1 because that's the border to the next detail level, and we want to include up to it.
|
||||
}
|
||||
|
||||
/** Given a section pos at level n this method returns the parent value at level n+1 */
|
||||
public final T getParentValue(DhSectionPos pos) { return this.get(pos.getParentPos()); }
|
||||
|
||||
/**
|
||||
* Given a section pos at level n and a child index, this returns the child section at level n-1
|
||||
* @param child0to3 since there are 4 possible children this index identifies which one we are getting
|
||||
*/
|
||||
public final T getChildValue(DhSectionPos pos, int child0to3) { return this.get(pos.getChildByIndex(child0to3)); }
|
||||
|
||||
|
||||
|
||||
//==============//
|
||||
// base methods //
|
||||
//==============//
|
||||
|
||||
public boolean isEmpty() { return this.leafNodeCount() == 0; } // TODO this should be rewritten to short-circuit
|
||||
|
||||
public int leafNodeCount()
|
||||
{
|
||||
AtomicInteger count = new AtomicInteger(0);
|
||||
this.topRingList.forEachPos((node, pos) ->
|
||||
{
|
||||
if (node != null)
|
||||
{
|
||||
node.forAllLeafValues((value) -> { count.addAndGet(1); });
|
||||
}
|
||||
});
|
||||
|
||||
return count.get();
|
||||
}
|
||||
|
||||
public int width() { return this.topRingList.getWidth(); }
|
||||
|
||||
// public String getDebugString()
|
||||
// {
|
||||
// StringBuilder sb = new StringBuilder();
|
||||
// for (byte i = 0; i < this.ringLists.length; i++)
|
||||
// {
|
||||
// sb.append("Layer ").append(i + TREE_LOWEST_DETAIL_LEVEL).append(":\n");
|
||||
// sb.append(this.ringLists[i].toDetailString());
|
||||
// sb.append("\n");
|
||||
// sb.append("\n");
|
||||
// }
|
||||
// return sb.toString();
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
|
||||
* licensed under the GNU LGPL v3 License.
|
||||
*
|
||||
* Copyright (C) 2020-2022 James Seibel
|
||||
*
|
||||
* This program 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, version 3.
|
||||
*
|
||||
* This program 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 Lesser 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, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package tests;
|
||||
|
||||
import com.seibel.lod.core.logging.DhLoggerBuilder;
|
||||
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.QuadTree;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.config.Configurator;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* This is just a quick demo to confirm the testing system is set up correctly.
|
||||
*
|
||||
* @author James Seibel
|
||||
* @version 2022-9-5
|
||||
*/
|
||||
public class QuadTreeTest
|
||||
{
|
||||
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
|
||||
|
||||
private static final int ROOT_NODE_WIDTH_IN_BLOCKS = BitShiftUtil.powerOfTwo(10);
|
||||
private static final int MIN_TREE_WIDTH_IN_BLOCKS = ROOT_NODE_WIDTH_IN_BLOCKS * 8;
|
||||
|
||||
static
|
||||
{
|
||||
Configurator.setRootLevel(Level.ALL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void SectionPosTest()
|
||||
{
|
||||
DhSectionPos root = new DhSectionPos((byte)10, 0, 0);
|
||||
DhSectionPos child = new DhSectionPos((byte)9, 1, 1);
|
||||
|
||||
Assert.assertTrue("section pos contains fail", root.contains(child));
|
||||
Assert.assertFalse("section pos contains fail", child.contains(root));
|
||||
|
||||
|
||||
root = new DhSectionPos((byte)10, 1, 0);
|
||||
child = new DhSectionPos((byte)9, 1, 1);
|
||||
Assert.assertFalse("section pos contains fail", root.contains(child));
|
||||
child = new DhSectionPos((byte)9, 2, 2);
|
||||
Assert.assertTrue("section pos contains fail", root.contains(child));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void BasicPositiveQuadTreeTest()
|
||||
{
|
||||
QuadTree<Integer> tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0));
|
||||
|
||||
|
||||
// root node //
|
||||
testSet(tree, new DhSectionPos((byte)10, 0, 0), 0);
|
||||
|
||||
// first child (0,0) //
|
||||
testSet(tree, new DhSectionPos((byte)9, 0, 0), 1);
|
||||
testSet(tree, new DhSectionPos((byte)9, 1, 0), 2);
|
||||
testSet(tree, new DhSectionPos((byte)9, 0, 1), 3);
|
||||
testSet(tree, new DhSectionPos((byte)9, 1, 1), 4);
|
||||
|
||||
// second child (0,0) (0,0) //
|
||||
testSet(tree, new DhSectionPos((byte)8, 0, 0), 5);
|
||||
testSet(tree, new DhSectionPos((byte)8, 1, 0), 6);
|
||||
testSet(tree, new DhSectionPos((byte)8, 0, 1), 7);
|
||||
testSet(tree, new DhSectionPos((byte)8, 1, 1), 8);
|
||||
// second child (0,0) (1,1) //
|
||||
testSet(tree, new DhSectionPos((byte)8, 2, 2), 9);
|
||||
testSet(tree, new DhSectionPos((byte)8, 3, 2), 10);
|
||||
testSet(tree, new DhSectionPos((byte)8, 2, 3), 11);
|
||||
testSet(tree, new DhSectionPos((byte)8, 3, 3), 12);
|
||||
|
||||
// third child (0,0) (1,0) (0,0) //
|
||||
testSet(tree, new DhSectionPos((byte)7, 5, 0), 9);
|
||||
testSet(tree, new DhSectionPos((byte)7, 6, 0), 10);
|
||||
testSet(tree, new DhSectionPos((byte)7, 5, 1), 11);
|
||||
testSet(tree, new DhSectionPos((byte)7, 6, 1), 12);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void BasicNegativeQuadTreeTest()
|
||||
{
|
||||
QuadTree<Integer> tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0));
|
||||
|
||||
|
||||
// root node //
|
||||
testSet(tree, new DhSectionPos((byte)10, -1, -1), 0);
|
||||
|
||||
// first child (-1,-1) //
|
||||
testSet(tree, new DhSectionPos((byte)9, -2, -1), 1);
|
||||
testSet(tree, new DhSectionPos((byte)9, -1, -1), 2);
|
||||
testSet(tree, new DhSectionPos((byte)9, -2, -2), 3);
|
||||
testSet(tree, new DhSectionPos((byte)9, -1, -2), 4);
|
||||
|
||||
// TODO
|
||||
// // second child (-1,-1) (0,0) //
|
||||
// runTest(tree, new DhSectionPos((byte)8, 0, 0), 5);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 1, 0), 6);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 0, 1), 7);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 1, 1), 8);
|
||||
// // second child (-1,-1) (1,1) //
|
||||
// runTest(tree, new DhSectionPos((byte)8, 2, 2), 9);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 3, 2), 10);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 2, 3), 11);
|
||||
// runTest(tree, new DhSectionPos((byte)8, 3, 3), 12);
|
||||
//
|
||||
// // third child (-1,-1) (1,0) (0,0) //
|
||||
// runTest(tree, new DhSectionPos((byte)7, 5, 0), 9);
|
||||
// runTest(tree, new DhSectionPos((byte)7, 6, 0), 10);
|
||||
// runTest(tree, new DhSectionPos((byte)7, 5, 1), 11);
|
||||
// runTest(tree, new DhSectionPos((byte)7, 6, 1), 12);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void QuadTreeMovingTest()
|
||||
{
|
||||
int treeWidthInRootNodes = 8;
|
||||
int treeWidthInBlocks = ROOT_NODE_WIDTH_IN_BLOCKS * treeWidthInRootNodes;
|
||||
QuadTree<Integer> tree = new QuadTree<>(treeWidthInBlocks, new DhBlockPos2D(0, 0));
|
||||
|
||||
|
||||
|
||||
// root nodes //
|
||||
testSet(tree, new DhSectionPos((byte)10, 0, 0), 1);
|
||||
|
||||
// first child (0,0) //
|
||||
DhSectionPos nw = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 0, 0);
|
||||
DhSectionPos ne = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 1, 0);
|
||||
DhSectionPos sw = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 0, 1);
|
||||
DhSectionPos se = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 1, 1);
|
||||
|
||||
testSet(tree, nw, 2);
|
||||
testSet(tree, ne, 3);
|
||||
testSet(tree, sw, 4);
|
||||
testSet(tree, se, 5);
|
||||
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4);
|
||||
|
||||
|
||||
// fake move //
|
||||
tree.setCenterBlockPos(DhBlockPos2D.ZERO);
|
||||
Assert.assertEquals("Tree center incorrect", DhBlockPos2D.ZERO, tree.getCenterBlockPos());
|
||||
|
||||
testGet(tree, nw, 2);
|
||||
testGet(tree, ne, 3);
|
||||
testGet(tree, sw, 4);
|
||||
testGet(tree, se, 5);
|
||||
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4);
|
||||
|
||||
|
||||
// small move //
|
||||
DhBlockPos2D smallMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS *2, 0); // move enough that the original root nodes aren't touching the same grid squares they were before, but not far enough as to be garbage collected (TODO reword)
|
||||
tree.setCenterBlockPos(smallMoveBlockPos);
|
||||
Assert.assertEquals("Tree center incorrect", smallMoveBlockPos, tree.getCenterBlockPos());
|
||||
|
||||
// nodes should be found at the same locations
|
||||
testGet(tree, nw, 2);
|
||||
testGet(tree, ne, 3);
|
||||
testGet(tree, sw, 4);
|
||||
testGet(tree, se, 5);
|
||||
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4);
|
||||
|
||||
|
||||
|
||||
// big move //
|
||||
DhBlockPos2D bigMoveBlockPos = new DhBlockPos2D(treeWidthInBlocks * 2, 0);
|
||||
tree.setCenterBlockPos(bigMoveBlockPos);
|
||||
Assert.assertEquals("Tree center incorrect", bigMoveBlockPos, tree.getCenterBlockPos());
|
||||
|
||||
// nothing should be found in the tree
|
||||
Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, nw, null));
|
||||
Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, ne, null));
|
||||
Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, sw, null));
|
||||
Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, se, null));
|
||||
|
||||
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 0);
|
||||
|
||||
|
||||
|
||||
// edge move //
|
||||
|
||||
// move back to the origin for easy testing
|
||||
tree.setCenterBlockPos(DhBlockPos2D.ZERO);
|
||||
Assert.assertEquals("Tree center incorrect", DhBlockPos2D.ZERO, tree.getCenterBlockPos());
|
||||
|
||||
// TODO move me
|
||||
DhSectionPos outOfBoundsPos = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, ROOT_NODE_WIDTH_IN_BLOCKS, 0); // wrong detail level on purpose, if the detail level was 0 (block) this should work
|
||||
Assert.assertThrows("incorrect exception thrown ", IndexOutOfBoundsException.class, () -> testSet(tree, outOfBoundsPos, 2));
|
||||
Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount());
|
||||
|
||||
// 1 root node from the edge
|
||||
DhSectionPos edgePos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, -((treeWidthInBlocks/2)-ROOT_NODE_WIDTH_IN_BLOCKS), 0);
|
||||
testSet(tree, edgePos, 2);
|
||||
Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount());
|
||||
|
||||
// edge move
|
||||
DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS, 0); // TODO I can only move this 1 root node away from the center for some reason
|
||||
tree.setCenterBlockPos(edgeMoveBlockPos);
|
||||
Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos());
|
||||
|
||||
Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount());
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void QuadTreeIterationTest()
|
||||
{
|
||||
QuadTree<Integer> tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0));
|
||||
|
||||
|
||||
// root nodes //
|
||||
testSet(tree, new DhSectionPos((byte)10, 0, 0), 1);
|
||||
testSet(tree, new DhSectionPos((byte)10, 1, 0), 2);
|
||||
|
||||
// first child (0,0) //
|
||||
testSet(tree, new DhSectionPos((byte)9, 0, 0), 3);
|
||||
testSet(tree, new DhSectionPos((byte)9, 1, 0), 4);
|
||||
testSet(tree, new DhSectionPos((byte)9, 0, 1), 5);
|
||||
testSet(tree, new DhSectionPos((byte)9, 1, 1), 6);
|
||||
|
||||
|
||||
final AtomicInteger rootNodeCount = new AtomicInteger(0);
|
||||
final AtomicInteger leafCount = new AtomicInteger(0);
|
||||
final AtomicInteger leafValueSum = new AtomicInteger(0);
|
||||
tree.forEachRootNode((rootNode) ->
|
||||
{
|
||||
rootNodeCount.addAndGet(1);
|
||||
|
||||
rootNode.forAllLeafValues((leafValue) ->
|
||||
{
|
||||
leafCount.addAndGet(1);
|
||||
leafValueSum.addAndGet(leafValue);
|
||||
});
|
||||
});
|
||||
|
||||
Assert.assertEquals("incorrect root count", 2, rootNodeCount.get());
|
||||
Assert.assertEquals("incorrect leaf count", 5, leafCount.get());
|
||||
Assert.assertEquals("incorrect leaf value sum", 20, leafValueSum.get());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void testSet(QuadTree<Integer> tree, DhSectionPos pos, Integer value)
|
||||
{
|
||||
// set
|
||||
Integer setResult = tree.set(pos, value);
|
||||
Assert.assertNull("set failed "+pos, setResult);
|
||||
// get
|
||||
Integer getResult = tree.get(pos);
|
||||
Assert.assertEquals("get failed "+pos, value, getResult);
|
||||
}
|
||||
|
||||
private static void testGet(QuadTree<Integer> tree, DhSectionPos pos, Integer value)
|
||||
{
|
||||
// get
|
||||
Integer getResult = tree.get(pos);
|
||||
Assert.assertEquals("get failed "+pos, value, getResult);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user