#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2011 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if ENABLE_QUADTREES
#if UNITTEST
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework;
using NUnit.Framework;
using Nuclex.Support.Plugins;
namespace Nuclex.Game.Space {
/// Unit tests for the quad-tree index
[TestFixture]
internal class QuadTreeTest {
#region class TestItem
/// Simple test item used to test the quad-tree
private class TestItem : IBoundingRectangleProvider {
/// Initializes a new test item
/// Bounding rectangle of the test item
public TestItem(BoundingRectangle rectangle) {
this.boundingRectangle = rectangle;
}
/// Bounding rectangle of the test item
public BoundingRectangle BoundingRectangle {
get {
++this.BoundRectangleLookups;
return this.boundingRectangle;
}
}
/// Number of times the bounding rectangles has been queried
public int BoundRectangleLookups;
/// The test item's bounding rectangle
private BoundingRectangle boundingRectangle;
}
#endregion // class TestItem
///
/// Verifies that the quad-tree's simplified constructor is working
///
[Test]
public void TestSimpleConstructor() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
Assert.IsNotNull(quadTree); // nonsense; avoids compiler warning
}
///
/// Verifies that the quad-tree's full constructor is working
///
[Test]
public void TestFullConstructor() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 4;
Assert.IsNotNull(quadTree); // nonsense; avoids compiler warning
}
///
/// Tests whether the quad-tree splits the root node if too many items are
/// added to it
///
[Test]
public void TestInsertSplitting() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 6;
TestItem[] items = new TestItem[] {
// Unsplittable node to test FirstSplittableItemIndex
new TestItem(new BoundingRectangle(-10.0f, -10.0f, 10.0f, 10.0f)),
// Four corners
new TestItem(new BoundingRectangle(-90.0f, -90.0f, -10.0f, -10.0f)),
new TestItem(new BoundingRectangle(10.0f, -90.0f, 90.0f, -10.0f)),
new TestItem(new BoundingRectangle(-90.0f, 10.0f, -10.0f, 90.0f)),
new TestItem(new BoundingRectangle(10.0f, 10.0f, 90.0f, 90.0f)),
// Unsplittable node to test split skipping
new TestItem(new BoundingRectangle(-15.0f, -15.0f, 15.0f, 15.0f)),
// Causes the node to split
new TestItem(new BoundingRectangle(-90.0f, -90.0f, 90.0f, 90.0f)),
// To test insertion into child nodes
new TestItem(new BoundingRectangle(-5.0f, -5.0f, 5.0f, 5.0f)),
new TestItem(new BoundingRectangle(-90.0f, -90.0f, -85.0f, -85.0f)),
new TestItem(new BoundingRectangle(85.0f, -90.0f, 90.0f, -85.0f)),
new TestItem(new BoundingRectangle(-90.0f, 85.0f, -85.0f, 90.0f)),
new TestItem(new BoundingRectangle(85.0f, 85.0f, 90.0f, 90.0f))
};
foreach(TestItem item in items) {
quadTree.Insert(item);
}
// Remember the current bounding rectangle lookups for the items which
// should be situated in other quad-tree nodes by now
int lookupCount2 = items[2].BoundRectangleLookups;
int lookupCount3 = items[3].BoundRectangleLookups;
int lookupCount4 = items[4].BoundRectangleLookups;
// Perform the query
List result = new List();
quadTree.Query(new BoundingRectangle(-80.0f, -80.0f, -20.0f, -20.0f), result);
// Make sure that the quad-tree didn't check the items that should have
// been placed into other nodes
Assert.AreEqual(lookupCount2, items[2].BoundRectangleLookups);
Assert.AreEqual(lookupCount3, items[3].BoundRectangleLookups);
Assert.AreEqual(lookupCount4, items[4].BoundRectangleLookups);
// Make sure the query results are correct
Assert.AreEqual(2, result.Count);
Assert.Contains(items[1], result);
Assert.Contains(items[6], result);
}
///
/// Verifies that the quad-tree can handle unsplittable items in its nodes
///
[Test]
public void TestInsertUnsplittable() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 3;
for(int index = 0; index < 6; ++index) {
quadTree.Insert(
new TestItem(new BoundingRectangle(-10.0f, -10.0f, 10.0f, 10.0f))
);
}
}
/// Verifies that the tree can handle external items
[Test]
public void TestQueryExternalItems() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 2;
TestItem[] items = new TestItem[] {
new TestItem(new BoundingRectangle(-90.0f, -110.0f, -89.9f, -109.9f)), // 0: u1
new TestItem(new BoundingRectangle(-80.0f, -110.0f, -79.9f, -109.9f)), // 1: u2
new TestItem(new BoundingRectangle(80.0f, -110.0f, 80.1f, -109.9f)), // 2: u3
new TestItem(new BoundingRectangle(90.0f, -110.0f, 90.1f, -109.9f)), // 3: u4
new TestItem(new BoundingRectangle(-110.0f, -90.0f, -109.9f, -89.9f)), // 4: l1
new TestItem(new BoundingRectangle(-110.0f, -80.0f, -109.9f, -79.9f)), // 5: l2
new TestItem(new BoundingRectangle(-110.0f, 80.0f, -109.9f, 80.1f)), // 6: l3
new TestItem(new BoundingRectangle(-110.0f, 90.0f, -109.9f, 90.1f)), // 7: l4
new TestItem(new BoundingRectangle(-90.0f, 110.0f, -89.9f, 110.1f)), // 8: b1
new TestItem(new BoundingRectangle(-80.0f, 110.0f, -79.9f, 110.1f)), // 9: b2
new TestItem(new BoundingRectangle(80.0f, 110.0f, 80.1f, 110.1f)), // 10: b3
new TestItem(new BoundingRectangle(90.0f, 110.0f, 90.1f, 110.1f)), // 11: b4
new TestItem(new BoundingRectangle(110.0f, -90.0f, 110.1f, -89.9f)), // 12: r1
new TestItem(new BoundingRectangle(110.0f, -80.0f, 110.1f, -79.9f)), // 13: r2
new TestItem(new BoundingRectangle(110.0f, 80.0f, 110.1f, 80.1f)), // 14: r3
new TestItem(new BoundingRectangle(110.0f, 90.0f, 110.1f, 90.1f)) // 15: r4
};
foreach(TestItem item in items) {
quadTree.Insert(item);
}
List result = new List();
//
// Perform some queries that would cause a normal quadtree to query all of
// the contained items (because they're all outside the bounds of the tree)
//
// Upper left
int lookupCount2 = items[2].BoundRectangleLookups;
int lookupCount6 = items[6].BoundRectangleLookups;
quadTree.Query(new BoundingRectangle(-95.0f, -95.0f, -85.0f, -85.0f), result);
Assert.AreEqual(lookupCount2, items[2].BoundRectangleLookups);
Assert.AreEqual(lookupCount6, items[6].BoundRectangleLookups);
// Upper right
int lookupCount1 = items[1].BoundRectangleLookups;
int lookupCount14 = items[14].BoundRectangleLookups;
quadTree.Query(new BoundingRectangle(85.0f, -95.0f, 95.0f, -85.0f), result);
Assert.AreEqual(lookupCount1, items[1].BoundRectangleLookups);
Assert.AreEqual(lookupCount14, items[14].BoundRectangleLookups);
// Lower left
int lookupCount5 = items[5].BoundRectangleLookups;
int lookupCount10 = items[10].BoundRectangleLookups;
quadTree.Query(new BoundingRectangle(-95.0f, 85.0f, -85.0f, 95.0f), result);
Assert.AreEqual(lookupCount5, items[5].BoundRectangleLookups);
Assert.AreEqual(lookupCount10, items[10].BoundRectangleLookups);
// Lower right
int lookupCount9 = items[9].BoundRectangleLookups;
int lookupCount13 = items[13].BoundRectangleLookups;
quadTree.Query(new BoundingRectangle(85.0f, 85.0f, 95.0f, 95.0f), result);
Assert.AreEqual(lookupCount9, items[9].BoundRectangleLookups);
Assert.AreEqual(lookupCount13, items[13].BoundRectangleLookups);
}
/// Verifies that the tree can handle external items
[Test]
public void TestQueryEntireNodes() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 2;
TestItem[] items = new TestItem[] {
new TestItem(new BoundingRectangle(-30.0f, -30.0f, -29.9f, -29.9f)), // 0: tl-a
new TestItem(new BoundingRectangle(-20.0f, -20.0f, -19.9f, -19.9f)), // 1: tl-b
new TestItem(new BoundingRectangle(-10.0f, -10.0f, -9.9f, -9.9f)), // 2: tl-c
new TestItem(new BoundingRectangle(30.0f, -30.0f, 30.1f, -29.9f)), // 3: tr-a
new TestItem(new BoundingRectangle(20.0f, -20.0f, 20.1f, -19.9f)), // 4: tr-b
new TestItem(new BoundingRectangle(10.0f, -10.0f, 10.1f, -9.9f)), // 5: tr-c
new TestItem(new BoundingRectangle(-30.0f, 30.0f, -29.9f, 30.1f)), // 6: br-a
new TestItem(new BoundingRectangle(-20.0f, 20.0f, -19.9f, 20.1f)), // 7: bl-b
new TestItem(new BoundingRectangle(-10.0f, 10.0f, -9.9f, 10.1f)), // 8: bl-c
new TestItem(new BoundingRectangle(30.0f, 30.0f, 30.1f, 30.1f)), // 9: br-a
new TestItem(new BoundingRectangle(20.0f, 20.0f, 20.1f, 20.1f)), // 10: br-b
new TestItem(new BoundingRectangle(10.0f, 10.0f, 10.1f, 10.1f)), // 11: br-c
new TestItem(new BoundingRectangle(21.0f, 21.0f, 21.1f, 21.1f)), // 12: c-a
new TestItem(new BoundingRectangle(22.0f, 22.0f, 22.1f, 22.1f)), // 13: c-b
new TestItem(new BoundingRectangle(23.0f, 23.0f, 23.1f, 23.1f)) // 14: c-c
};
foreach(TestItem item in items) {
quadTree.Insert(item);
}
// Perform some queries that would cause a normal quadtree to query all of
// the contained items (because they're all outside the bounds of the tree)
List result = new List();
quadTree.Query(new BoundingRectangle(-60.0f, -60.0f, -15.0f, -15.0f), result);
quadTree.Query(new BoundingRectangle(15.0f, -60.0f, 60.0f, -15.0f), result);
quadTree.Query(new BoundingRectangle(-60.0f, 15.0f, -15.0f, 60.0f), result);
quadTree.Query(new BoundingRectangle(15.0f, 15.0f, 60.0f, 60.0f), result);
}
///
/// Tests whether the quad-tree correctly handles an overflowing node that
/// can not be split
///
[Test]
public void TestIncreaseCapacityDuringInsertion() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 2;
TestItem[] items = new TestItem[] {
new TestItem(new BoundingRectangle(-10.0f, -10.0f, 10.0f, 10.0f)),
new TestItem(new BoundingRectangle(-20.0f, -20.0f, 20.0f, 20.0f)),
new TestItem(new BoundingRectangle(-30.0f, -30.0f, 30.0f, 30.0f)),
new TestItem(new BoundingRectangle(-40.0f, -40.0f, 40.0f, 40.0f))
};
foreach(TestItem item in items) {
quadTree.Insert(item);
}
}
///
/// Tests whether the quad-tree correctly splits an overflowed node
///
[Test]
public void TestIncreaseCapacityDuringSplit() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 5;
TestItem[] items = new TestItem[] {
new TestItem(new BoundingRectangle(-60.0f, -60.0f, -40.0f, -40.0f)),
new TestItem(new BoundingRectangle(-70.0f, -70.0f, -30.0f, -30.0f)),
new TestItem(new BoundingRectangle(-80.0f, -80.0f, -20.0f, -20.0f)),
new TestItem(new BoundingRectangle(-90.0f, -90.0f, -10.0f, -10.0f)),
new TestItem(new BoundingRectangle(-160.0f, -160.0f, -140.0f, -140.0f)),
new TestItem(new BoundingRectangle(-170.0f, -170.0f, -130.0f, -130.0f)),
new TestItem(new BoundingRectangle(-180.0f, -180.0f, -120.0f, -120.0f)),
new TestItem(new BoundingRectangle(-190.0f, -190.0f, -110.0f, -110.0f))
};
foreach(TestItem item in items) {
quadTree.Insert(item);
}
}
///
/// Tests whether the quad-tree can cope with an attempt to remove an object
/// that it doesn't contain
///
[Test]
public void TestUnindexedItemRemoval() {
QuadTree quadTree = new QuadTree(
new BoundingRectangle(-100.0f, -100.0f, 100.0f, 100.0f)
);
quadTree.MaxItemsPerNode = 32;
Assert.IsFalse(
quadTree.Remove(new TestItem(new BoundingRectangle()))
);
}
}
} // namespace Nuclex.Game.Space
#endif // UNITTEST
#endif // ENABLE_QUADTREES