getRoot() {
171 | return root;
172 | }
173 |
174 |
175 | /**
176 | * Different methods for splitting nodes in an RTree.
177 | *
178 | * AXIAL has been shown to give good performance for many general spatial problems,
179 | *
180 | *
181 | * Created by ewhite on 10/28/15.
182 | */
183 | public enum Split {
184 | AXIAL,
185 | LINEAR,
186 | QUADRATIC,
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/main/java/com/conversantmedia/util/collection/spatial/RectBuilder.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | /**
24 | * Created by jcairns on 4/30/15.
25 | */
26 | public interface RectBuilder {
27 |
28 | /**
29 | * Build a bounding rectangle for the given element
30 | *
31 | * @param t - element to bound
32 | *
33 | * @return HyperRect impl for this entry
34 | */
35 | HyperRect getBBox(T t);
36 |
37 |
38 | /**
39 | * Build a bounding rectangle for given points (min and max, usually)
40 | *
41 | * @param p1 - first point (top-left point, for example)
42 | * @param p2 - second point (bottom-right point, for example)
43 | *
44 | * @return HyperRect impl defined by two points
45 | */
46 | HyperRect getMbr(HyperPoint p1, HyperPoint p2);
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/conversantmedia/util/collection/spatial/SpatialSearch.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import java.util.function.Consumer;
24 | import java.util.Collection;
25 |
26 | /**
27 | * Created by jcovert on 12/30/15.
28 | */
29 | public interface SpatialSearch {
30 | /**
31 | * Search for entries intersecting given bounding rect
32 | *
33 | * @param rect - Bounding rectangle to use for querying
34 | * @param t - Array to store found entries
35 | *
36 | * @return Number of results found
37 | */
38 | int intersects(HyperRect rect, T[] t);
39 |
40 | /**
41 | * Search for entries intersecting given bounding rect
42 | *
43 | * @param rect - Bounding rectangle to use for querying
44 | * @param consumer - callback to receive intersecting objects
45 | *
46 | */
47 | void intersects(HyperRect rect, Consumer consumer);
48 |
49 | /**
50 | * Search for entries contained by the given bounding rect
51 | *
52 | * @param rect - Bounding rectangle to use for querying
53 | * @param t - Array to store found entries
54 | *
55 | * @return Number of results found
56 | */
57 | int search(HyperRect rect, T[] t);
58 |
59 | /**
60 | * Search for entries contained by the given bounding rect
61 | *
62 | * @param rect - Bounding rectangle to use for querying
63 | * @param consumer - callback to receive intersecting objects
64 | *
65 | */
66 | void search(HyperRect rect, Consumer consumer);
67 |
68 | /**
69 | * Search for entries contained by the given bounding rect
70 | *
71 | * @param rect - Bounding rectangle to use for querying
72 | * @param collection - collection to receive results
73 | *
74 | */
75 | void search(HyperRect rect, Collection collection);
76 |
77 | /**
78 | * returns whether or not the HyperRect will enclose all of the data entries in t
79 | *
80 | * @param t - entry
81 | *
82 | * @return boolean - Whether or not all entries lie inside rect
83 | */
84 | boolean contains(T t);
85 |
86 | /**
87 | * Add the data entry to the SpatialSearch structure
88 | *
89 | * @param t Data entry to be added
90 | */
91 | void add(T t);
92 |
93 | /**
94 | * Remove the data entry from the SpatialSearch structure
95 | *
96 | * @param t Data entry to be removed
97 | */
98 | void remove(T t);
99 |
100 | /**
101 | * Update entry in tree
102 | *
103 | * @param told - Entry to update
104 | * @param tnew - Entry to update it to
105 | */
106 | void update(T told, T tnew);
107 |
108 | /**
109 | * Get the number of entries in the tree
110 | *
111 | * @return entry count
112 | */
113 | int getEntryCount();
114 |
115 | /**
116 | * Iterate over all entries in the tree
117 | *
118 | * @param consumer - callback for each element
119 | */
120 | void forEach(Consumer consumer);
121 |
122 | Stats collectStats();
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/com/conversantmedia/util/collection/spatial/SpatialSearches.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import java.util.concurrent.locks.ReentrantReadWriteLock;
24 |
25 | /**
26 | * Create instances of SpatialSearch implementations
27 | *
28 | * Created by jcovert on 2/3/16.
29 | */
30 | public class SpatialSearches {
31 |
32 | private static final int DEFAULT_MIN_M = 2;
33 | private static final int DEFAULT_MAX_M = 8;
34 | private static final RTree.Split DEFAULT_SPLIT_TYPE = RTree.Split.AXIAL;
35 |
36 | private SpatialSearches() {}
37 |
38 | /**
39 | * Create an R-Tree with default values for m, M, and split type
40 | *
41 | * @param builder - Builder implementation used to create HyperRects out of T's
42 | * @param - The store type of the bound
43 | *
44 | * @return SpatialSearch - The spatial search and index structure
45 | */
46 | public static SpatialSearch rTree(final RectBuilder builder) {
47 | return new RTree<>(builder, DEFAULT_MIN_M, DEFAULT_MAX_M, DEFAULT_SPLIT_TYPE);
48 | }
49 |
50 | /**
51 | * Create an R-Tree with specified values for m, M, and split type
52 | *
53 | * @param builder - Builder implementation used to create HyperRects out of T's
54 | * @param minM - minimum number of entries per node of this tree
55 | * @param maxM - maximum number of entries per node of this tree (exceeding this causes node split)
56 | * @param splitType - type of split to use when M+1 entries are added to a node
57 | * @param - The store type of the bound
58 | *
59 | * @return SpatialSearch - The spatial search and index structure
60 | */
61 | public static SpatialSearch rTree(final RectBuilder builder, final int minM, final int maxM, final RTree.Split splitType) {
62 | return new RTree<>(builder, minM, maxM, splitType);
63 | }
64 |
65 | /**
66 | * Create a protected R-Tree with default values for m, M, and split type
67 | *
68 | * @param builder - Builder implementation used to create HyperRects out of T's
69 | * @param - The store type of the bound
70 | *
71 | * @return SpatialSearch - The spatial search and index structure
72 | */
73 | public static SpatialSearch lockingRTree(final RectBuilder builder) {
74 | return new ConcurrentRTree<>(rTree(builder), new ReentrantReadWriteLock(true));
75 | }
76 |
77 | /**
78 | * Create a protected R-Tree with specified values for m, M, and split type
79 | *
80 | * @param builder - Builder implementation used to create HyperRects out of T's
81 | * @param minM - minimum number of entries per node of this tree
82 | * @param maxM - maximum number of entries per node of this tree (exceeding this causes node split)
83 | * @param splitType - type of split to use when M+1 entries are added to a node
84 | * @param - The store type of the bound
85 | *
86 | * @return SpatialSearch - The spatial search and index structure
87 | */
88 | public static SpatialSearch lockingRTree(final RectBuilder builder, final int minM, final int maxM, final RTree.Split splitType) {
89 | return new ConcurrentRTree<>(rTree(builder, minM, maxM, splitType), new ReentrantReadWriteLock(true));
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/com/conversantmedia/util/collection/spatial/Stats.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import java.io.PrintStream;
24 |
25 | /**
26 | * Created by jcovert on 5/20/15.
27 | */
28 | public class Stats {
29 |
30 | private RTree.Split type;
31 | private int maxFill;
32 | private int minFill;
33 |
34 | private int maxDepth = 0;
35 | private int branchCount = 0;
36 | private int leafCount = 0;
37 | private int entryCount = 0;
38 | private int[] entriesAtDepth = new int[1000];
39 | private int[] branchesAtDepth = new int[1000];
40 | private int[] leavesAtDepth = new int[1000];
41 |
42 | public void print(PrintStream out) {
43 | out.println("[" + type + "] m=" + minFill + " M=" + maxFill);
44 | out.println(" Branches (" + branchCount + " total)");
45 | out.print(" ");
46 | for (int i = 0; i <= maxDepth; i++) {
47 | out.print(i + ": " + branchesAtDepth[i] + " ");
48 | }
49 | out.println("\n Leaves (" + leafCount + " total)");
50 | out.print(" ");
51 | for (int i = 0; i <= maxDepth; i++) {
52 | out.print(i + ": " + leavesAtDepth[i] + " ");
53 | }
54 | out.println("\n Entries (" + entryCount + " total)");
55 | out.print(" ");
56 | for (int i = 0; i <= maxDepth; i++) {
57 | out.print(i + ": " + entriesAtDepth[i] + " ");
58 | }
59 | out.printf("\n Leaf Fill Percentage: %.2f%%\n", getLeafFillPercentage());
60 | out.printf(" Entries per Leaf: %.2f\n", getEntriesPerLeaf());
61 | out.println(" Max Depth: " + maxDepth);
62 | out.println();
63 | }
64 |
65 | public float getEntriesPerLeaf() {
66 | return ((entryCount * 1.0f) / leafCount);
67 | }
68 |
69 | public float getLeafFillPercentage() {
70 | return (getEntriesPerLeaf() * 100) / maxFill;
71 | }
72 |
73 | public RTree.Split getType() {
74 | return type;
75 | }
76 |
77 | public void setType(RTree.Split type) {
78 | this.type = type;
79 | }
80 |
81 | public void setMaxFill(int maxFill) {
82 | this.maxFill = maxFill;
83 | }
84 |
85 | public void setMinFill(int minFill) {
86 | this.minFill = minFill;
87 | }
88 |
89 | public int getBranchCount() {
90 | return branchCount;
91 | }
92 |
93 | public int getLeafCount() {
94 | return leafCount;
95 | }
96 |
97 | public int getEntryCount() {
98 | return entryCount;
99 | }
100 |
101 | public int getMaxDepth() {
102 | return maxDepth;
103 | }
104 |
105 | public void setMaxDepth(int maxDepth) {
106 | this.maxDepth = maxDepth;
107 | }
108 |
109 | public void countEntriesAtDepth(int entries, int depth) {
110 | entryCount += entries;
111 | entriesAtDepth[depth] += entries;
112 | }
113 |
114 | public void countLeafAtDepth(int depth) {
115 | leafCount++;
116 | leavesAtDepth[depth]++;
117 | }
118 |
119 | public void countBranchAtDepth(int depth) {
120 | branchCount++;
121 | branchesAtDepth[depth]++;
122 | }
123 | }
--------------------------------------------------------------------------------
/src/main/resources/RTree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/conversant/rtree/5123950dd43719664bb20f17f65515c9472fb49a/src/main/resources/RTree.png
--------------------------------------------------------------------------------
/src/main/resources/mM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/conversant/rtree/5123950dd43719664bb20f17f65515c9472fb49a/src/main/resources/mM.png
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/AxialSplitLeafTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Rect2d;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | import java.util.Random;
28 |
29 | /**
30 | * Created by jcovert on 6/12/15.
31 | */
32 | public class AxialSplitLeafTest {
33 |
34 | private static final RTree.Split TYPE = RTree.Split.AXIAL;
35 |
36 | /**
37 | * Adds enough entries to force a single split and confirms that
38 | * no entries are lost.
39 | */
40 | @Test
41 | public void basicSplitTest() {
42 |
43 | RTree rTree = RTreeTest.createRect2DTree(TYPE);
44 | rTree.add(new Rect2d(0, 0, 1, 1));
45 | rTree.add(new Rect2d(1, 1, 2, 2));
46 | rTree.add(new Rect2d(2, 2, 3, 3));
47 | rTree.add(new Rect2d(3, 3, 4, 4));
48 | rTree.add(new Rect2d(4, 4, 5, 5));
49 | rTree.add(new Rect2d(5, 5, 6, 6));
50 | rTree.add(new Rect2d(6, 6, 7, 7));
51 | rTree.add(new Rect2d(7, 7, 8, 8));
52 | // 9 entries guarantees a split
53 | rTree.add(new Rect2d(8, 8, 9, 9));
54 |
55 | Stats stats = rTree.collectStats();
56 | Assert.assertTrue("Unexpected max depth after basic split", stats.getMaxDepth() == 1);
57 | Assert.assertTrue("Unexpected number of branches after basic split", stats.getBranchCount() == 1);
58 | Assert.assertTrue("Unexpected number of leaves after basic split", stats.getLeafCount() == 2);
59 | Assert.assertTrue("Unexpected number of entries per leaf after basic split", stats.getEntriesPerLeaf() == 4.5);
60 | }
61 |
62 | @Test
63 | public void splitCorrectnessTest() {
64 |
65 | RTree rTree = RTreeTest.createRect2DTree(2, 4, TYPE);
66 | rTree.add(new Rect2d(0, 0, 3, 3));
67 | rTree.add(new Rect2d(1, 1, 2, 2));
68 | rTree.add(new Rect2d(2, 2, 4, 4));
69 | rTree.add(new Rect2d(4, 0, 5, 1));
70 | // 5 entrees guarantees a split
71 | rTree.add(new Rect2d(0, 2, 1, 4));
72 |
73 | Branch root = (Branch) rTree.getRoot();
74 | Node[] children = root.getChildren();
75 | int childCount = 0;
76 | for(Node c : children) {
77 | if (c != null) {
78 | childCount++;
79 | }
80 | }
81 | Assert.assertEquals("Expected different number of children after split", 2, childCount);
82 |
83 | Node child1 = children[0];
84 | Rect2d child1Mbr = (Rect2d) child1.getBound();
85 | Rect2d expectedChild1Mbr = new Rect2d(0, 0, 3, 4);
86 | Assert.assertEquals("Child 1 size incorrect after split", 3, child1.size());
87 | Assert.assertEquals("Child 1 mbr incorrect after split", expectedChild1Mbr, child1Mbr);
88 |
89 | Node child2 = children[1];
90 | Rect2d child2Mbr = (Rect2d) child2.getBound();
91 | Rect2d expectedChild2Mbr = new Rect2d(2, 0, 5, 4);
92 | Assert.assertEquals("Child 2 size incorrect after split", 2, child2.size());
93 | Assert.assertEquals("Child 2 mbr incorrect after split", expectedChild2Mbr, child2Mbr);
94 | }
95 |
96 | /**
97 | * Adds several overlapping rectangles and confirms that no entries
98 | * are lost during insert/split.
99 | */
100 | @Test
101 | public void overlappingEntryTest() {
102 |
103 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
104 | rTree.add(new Rect2d(0, 0, 1, 1));
105 | rTree.add(new Rect2d(0, 0, 2, 2));
106 | rTree.add(new Rect2d(0, 0, 2, 2));
107 | rTree.add(new Rect2d(0, 0, 3, 3));
108 | rTree.add(new Rect2d(0, 0, 3, 3));
109 |
110 | rTree.add(new Rect2d(0, 0, 4, 4));
111 | rTree.add(new Rect2d(0, 0, 5, 5));
112 | rTree.add(new Rect2d(0, 0, 6, 6));
113 | rTree.add(new Rect2d(0, 0, 7, 7));
114 | rTree.add(new Rect2d(0, 0, 7, 7));
115 |
116 | rTree.add(new Rect2d(0, 0, 8, 8));
117 | rTree.add(new Rect2d(0, 0, 9, 9));
118 | rTree.add(new Rect2d(0, 1, 2, 2));
119 | rTree.add(new Rect2d(0, 1, 3, 3));
120 | rTree.add(new Rect2d(0, 1, 4, 4));
121 |
122 | rTree.add(new Rect2d(0, 1, 4, 4));
123 | rTree.add(new Rect2d(0, 1, 5, 5));
124 |
125 | // 17 entries guarantees *at least* 2 splits when max leaf size is 8
126 | final int expectedEntryCount = 17;
127 |
128 | final Stats stats = rTree.collectStats();
129 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + expectedEntryCount + " actual: " + stats.getEntryCount(), expectedEntryCount, stats.getEntryCount());
130 | }
131 |
132 | /**
133 | * Adds many random entries and confirm that no entries
134 | * are lost during insert/split.
135 | */
136 | @Test
137 | public void randomEntryTest() {
138 |
139 | final int entryCount = 50000;
140 | final Rect2d[] rects = RTreeTest.generateRandomRects(entryCount);
141 |
142 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
143 | for (int i = 0; i < rects.length; i++) {
144 | rTree.add(rects[i]);
145 | }
146 |
147 | final Stats stats = rTree.collectStats();
148 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + entryCount + " actual: " + stats.getEntryCount(), entryCount, stats.getEntryCount());
149 | stats.print(System.out);
150 | }
151 |
152 | /**
153 | * This test previously caused a StackOverflowException on LINEAR leaf.
154 | * It has since been fixed, but keeping the test here to ensure this leaf type
155 | * never falls victim to the same issue.
156 | */
157 | @Test
158 | public void causeLinearSplitOverflow() {
159 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
160 | final Random rand = new Random(13);
161 | for (int i = 0; i < 500; i++) {
162 | final int x1 = rand.nextInt(10);
163 | final int y1 = rand.nextInt(10);
164 | final int x2 = x1 + rand.nextInt(200);
165 | final int y2 = y1 + rand.nextInt(200);
166 |
167 | rTree.add(new Rect2d(x1, y1, x2, y2));
168 | }
169 | final Stats stats = rTree.collectStats();
170 | stats.print(System.out);
171 | }
172 |
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/ConcurrentRTreeTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Rect2d;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | import java.util.concurrent.TimeUnit;
28 | import java.util.concurrent.locks.Condition;
29 | import java.util.concurrent.locks.Lock;
30 | import java.util.concurrent.locks.ReadWriteLock;
31 | import java.util.function.Consumer;
32 | import java.util.Collection;
33 |
34 | import static org.mockito.Mockito.*;
35 |
36 | /**
37 | * Created by jcovert on 12/30/15.
38 | */
39 | public class ConcurrentRTreeTest {
40 |
41 | private static final Rect2d RECT_2_D_0 = new Rect2d(0, 0, 0, 0);
42 | private static final Rect2d RECT_2_D_1 = new Rect2d(1, 1, 1, 1);
43 |
44 | @Test
45 | public void testSearchLocking() {
46 |
47 | MockLock lock = new MockLock();
48 | MockSearch search = new MockSearch(lock);
49 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
50 |
51 | // asserts proper locking
52 | tree.search(RECT_2_D_0, new Rect2d[0]);
53 | }
54 |
55 | @Test
56 | public void testAddLocking() {
57 |
58 | MockLock lock = new MockLock();
59 | MockSearch search = new MockSearch(lock);
60 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
61 |
62 | // asserts proper locking
63 | tree.add(RECT_2_D_0);
64 | }
65 |
66 | @Test
67 | public void testRemoveLocking() {
68 |
69 | MockLock lock = new MockLock();
70 | MockSearch search = new MockSearch(lock);
71 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
72 |
73 | // asserts proper locking
74 | tree.remove(RECT_2_D_0);
75 | }
76 |
77 | @Test
78 | public void testUpdateLocking() {
79 |
80 | MockLock lock = new MockLock();
81 | MockSearch search = new MockSearch(lock);
82 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
83 |
84 | // asserts proper locking
85 | tree.update(RECT_2_D_0, RECT_2_D_1);
86 | }
87 |
88 | @Test
89 | public void testSearchLockingCount() {
90 |
91 | Lock readLock = mock(Lock.class);
92 | Lock writeLock = mock(Lock.class);
93 | ReadWriteLock lock = mock(ReadWriteLock.class);
94 | when(lock.readLock()).thenReturn(readLock);
95 | when(lock.writeLock()).thenReturn(writeLock);
96 |
97 | SpatialSearch search = RTreeTest.createRect2DTree(2, 8, RTree.Split.AXIAL);
98 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
99 |
100 | tree.search(RECT_2_D_0, new Rect2d[0]);
101 |
102 | verify(readLock, times(1)).lock();
103 | verify(readLock, times(1)).unlock();
104 | verify(writeLock, never()).lock();
105 | verify(writeLock, never()).unlock();
106 | }
107 |
108 | @Test
109 | public void testAddLockingCount() {
110 |
111 | Lock readLock = mock(Lock.class);
112 | Lock writeLock = mock(Lock.class);
113 | ReadWriteLock lock = mock(ReadWriteLock.class);
114 | when(lock.readLock()).thenReturn(readLock);
115 | when(lock.writeLock()).thenReturn(writeLock);
116 |
117 | SpatialSearch search = RTreeTest.createRect2DTree(2, 8, RTree.Split.AXIAL);
118 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
119 |
120 | tree.add(RECT_2_D_0);
121 |
122 | verify(readLock, never()).lock();
123 | verify(readLock, never()).unlock();
124 | verify(writeLock, times(1)).lock();
125 | verify(writeLock, times(1)).unlock();
126 | }
127 |
128 | @Test
129 | public void testRemoveLockingCount() {
130 |
131 | Lock readLock = mock(Lock.class);
132 | Lock writeLock = mock(Lock.class);
133 | ReadWriteLock lock = mock(ReadWriteLock.class);
134 | when(lock.readLock()).thenReturn(readLock);
135 | when(lock.writeLock()).thenReturn(writeLock);
136 |
137 | SpatialSearch search = RTreeTest.createRect2DTree(2, 8, RTree.Split.AXIAL);
138 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
139 |
140 | tree.remove(RECT_2_D_0);
141 |
142 | verify(readLock, never()).lock();
143 | verify(readLock, never()).unlock();
144 | verify(writeLock, times(1)).lock();
145 | verify(writeLock, times(1)).unlock();
146 | }
147 |
148 | @Test
149 | public void testUpdateLockingCount() {
150 |
151 | Lock readLock = mock(Lock.class);
152 | Lock writeLock = mock(Lock.class);
153 | ReadWriteLock lock = mock(ReadWriteLock.class);
154 | when(lock.readLock()).thenReturn(readLock);
155 | when(lock.writeLock()).thenReturn(writeLock);
156 |
157 | SpatialSearch search = RTreeTest.createRect2DTree(2, 8, RTree.Split.AXIAL);
158 | ConcurrentRTree tree = new ConcurrentRTree<>(search, lock);
159 |
160 | tree.update(RECT_2_D_0, RECT_2_D_1);
161 |
162 | verify(readLock, never()).lock();
163 | verify(readLock, never()).unlock();
164 | verify(writeLock, times(1)).lock();
165 | verify(writeLock, times(1)).unlock();
166 | }
167 |
168 | private static class MockLock implements ReadWriteLock {
169 | boolean isLocked = false;
170 | int readers = 0;
171 |
172 | @Override
173 | public Lock readLock() {
174 | return new Lock() {
175 |
176 | @Override
177 | public void lock() {
178 | Assert.assertFalse("Attempting to acquire read lock while write locked", isLocked);
179 | readers++;
180 | }
181 |
182 | @Override
183 | public void lockInterruptibly() throws InterruptedException {
184 | Assert.assertFalse("Attempting to acquire read lock while write locked", isLocked);
185 | readers++;
186 | }
187 |
188 | @Override
189 | public boolean tryLock() {
190 | Assert.assertFalse("Attempting to acquire read lock while write locked", isLocked);
191 | readers++;
192 | return true;
193 | }
194 |
195 | @Override
196 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
197 | Assert.assertFalse("Attempting to acquire read lock while write locked", isLocked);
198 | readers++;
199 | return true;
200 | }
201 |
202 | @Override
203 | public void unlock() {
204 | Assert.assertNotEquals("Attempting to unlock read lock without any readers", readers, 0);
205 | readers--;
206 | }
207 |
208 | @Override
209 | public Condition newCondition() {
210 | throw new UnsupportedOperationException();
211 | }
212 | };
213 | }
214 |
215 | @Override
216 | public Lock writeLock() {
217 | return new Lock() {
218 |
219 | @Override
220 | public void lock() {
221 | isLocked = true;
222 | Assert.assertEquals("Attempting to acquire write lock while readers are reading", readers, 0);
223 | }
224 |
225 | @Override
226 | public void lockInterruptibly() throws InterruptedException {
227 | isLocked = true;
228 | Assert.assertEquals("Attempting to acquire write lock while readers are reading", readers, 0);
229 | }
230 |
231 | @Override
232 | public boolean tryLock() {
233 | isLocked = true;
234 | Assert.assertEquals("Attempting to acquire write lock while readers are reading", readers, 0);
235 | return true;
236 | }
237 |
238 | @Override
239 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
240 | isLocked = true;
241 | Assert.assertEquals("Attempting to acquire write lock while readers are reading", readers, 0);
242 | return true;
243 | }
244 |
245 | @Override
246 | public void unlock() {
247 | Assert.assertTrue("Attempting to unlock write lock without any writers", isLocked);
248 | isLocked = false;
249 | Assert.assertEquals("Attempting to unlock write lock while readers are reading", readers, 0);
250 | }
251 |
252 | @Override
253 | public Condition newCondition() {
254 | throw new UnsupportedOperationException();
255 | }
256 | };
257 | }
258 | }
259 |
260 | private static final class MockSearch implements SpatialSearch {
261 | MockLock lock;
262 |
263 | public MockSearch(MockLock lock) {
264 | this.lock = lock;
265 | }
266 |
267 | @Override
268 | public int intersects(HyperRect rect, Object[] t) {
269 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
270 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
271 | return 0;
272 | }
273 |
274 | @Override
275 | public void intersects(HyperRect rect, Consumer consumer) {
276 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
277 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
278 | }
279 |
280 | @Override
281 | public int search(HyperRect rect, Object[] t) {
282 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
283 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
284 | return 0;
285 | }
286 |
287 | @Override
288 | public void search(HyperRect rect, Consumer consumer) {
289 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
290 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
291 | }
292 |
293 | @Override
294 | public void search(HyperRect rect, Collection collection) {
295 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
296 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
297 | }
298 |
299 | @Override
300 | public boolean contains(Object o) {
301 | Assert.assertNotEquals("Read lock should have reader while search in progress", lock.readers, 0);
302 | Assert.assertFalse("Attempting to read while writers are writing", lock.isLocked);
303 | return false;
304 | }
305 |
306 | @Override
307 | public void add(Object o) {
308 | Assert.assertEquals("Read lock should have no readers while write in progress", lock.readers, 0);
309 | Assert.assertTrue("Attempting to write without write lock", lock.isLocked);
310 | }
311 |
312 | @Override
313 | public void remove(Object o) {
314 | Assert.assertEquals("Read lock should have no readers while write in progress", lock.readers, 0);
315 | Assert.assertTrue("Attempting to write without write lock", lock.isLocked);
316 | }
317 |
318 | @Override
319 | public void update(Object told, Object tnew) {
320 | Assert.assertEquals("Read lock should have no readers while write in progress", lock.readers, 0);
321 | Assert.assertTrue("Attempting to write without write lock", lock.isLocked);
322 | }
323 |
324 | @Override
325 | public int getEntryCount() {
326 | return 0;
327 | }
328 |
329 | @Override
330 | public void forEach(Consumer consumer) {
331 |
332 | }
333 |
334 | @Override
335 | public Stats collectStats() {
336 | return null;
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/LinearSplitLeafTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Rect2d;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | import java.util.Random;
28 |
29 | /**
30 | * Created by jcovert on 6/12/15.
31 | */
32 | public class LinearSplitLeafTest {
33 |
34 | private static final RTree.Split TYPE = RTree.Split.LINEAR;
35 |
36 | /**
37 | * Adds enough entries to force a single split and confirms that
38 | * no entries are lost.
39 | */
40 | @Test
41 | public void basicSplitTest() {
42 |
43 | RTree rTree = RTreeTest.createRect2DTree(TYPE);
44 | rTree.add(new Rect2d(0, 0, 1, 1));
45 | rTree.add(new Rect2d(1, 1, 2, 2));
46 | rTree.add(new Rect2d(2, 2, 3, 3));
47 | rTree.add(new Rect2d(3, 3, 4, 4));
48 | rTree.add(new Rect2d(4, 4, 5, 5));
49 | rTree.add(new Rect2d(5, 5, 6, 6));
50 | rTree.add(new Rect2d(6, 6, 7, 7));
51 | rTree.add(new Rect2d(7, 7, 8, 8));
52 | // 9 entries guarantees a split
53 | rTree.add(new Rect2d(8, 8, 9, 9));
54 |
55 | Stats stats = rTree.collectStats();
56 | Assert.assertTrue("Unexpected max depth after basic split", stats.getMaxDepth() == 1);
57 | Assert.assertTrue("Unexpected number of branches after basic split", stats.getBranchCount() == 1);
58 | Assert.assertTrue("Unexpected number of leaves after basic split", stats.getLeafCount() == 2);
59 | Assert.assertTrue("Unexpected number of entries per leaf after basic split", stats.getEntriesPerLeaf() == 4.5);
60 | }
61 |
62 | @Test
63 | public void splitCorrectnessTest() {
64 |
65 | RTree rTree = RTreeTest.createRect2DTree(2, 4, TYPE);
66 | rTree.add(new Rect2d(0, 0, 3, 3));
67 | rTree.add(new Rect2d(1, 1, 2, 2));
68 | rTree.add(new Rect2d(2, 2, 4, 4));
69 | rTree.add(new Rect2d(4, 0, 5, 1));
70 | // 5 entrees guarantees a split
71 | rTree.add(new Rect2d(0, 2, 1, 4));
72 |
73 | Branch root = (Branch) rTree.getRoot();
74 | Node[] children = root.getChildren();
75 | int childCount = 0;
76 | for(Node c : children) {
77 | if (c != null) {
78 | childCount++;
79 | }
80 | }
81 | Assert.assertEquals("Expected different number of children after split", 2, childCount);
82 |
83 | Node child1 = children[0];
84 | Rect2d child1Mbr = (Rect2d) child1.getBound();
85 | Rect2d expectedChild1Mbr = new Rect2d(0, 0, 4, 4);
86 | Assert.assertEquals("Child 1 size incorrect after split", 4, child1.size());
87 | Assert.assertEquals("Child 1 mbr incorrect after split", expectedChild1Mbr, child1Mbr);
88 |
89 | Node child2 = children[1];
90 | Rect2d child2Mbr = (Rect2d) child2.getBound();
91 | Rect2d expectedChild2Mbr = new Rect2d(4, 0, 5, 1);
92 | Assert.assertEquals("Child 2 size incorrect after split", 1, child2.size());
93 | Assert.assertEquals("Child 2 mbr incorrect after split", expectedChild2Mbr, child2Mbr);
94 | }
95 |
96 | /**
97 | * Adds several overlapping rectangles and confirms that no entries
98 | * are lost during insert/split.
99 | */
100 | @Test
101 | public void overlappingEntryTest() {
102 |
103 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
104 | rTree.add(new Rect2d(0, 0, 1, 1));
105 | rTree.add(new Rect2d(0, 0, 2, 2));
106 | rTree.add(new Rect2d(0, 0, 2, 2));
107 | rTree.add(new Rect2d(0, 0, 3, 3));
108 | rTree.add(new Rect2d(0, 0, 3, 3));
109 |
110 | rTree.add(new Rect2d(0, 0, 4, 4));
111 | rTree.add(new Rect2d(0, 0, 5, 5));
112 | rTree.add(new Rect2d(0, 0, 6, 6));
113 | rTree.add(new Rect2d(0, 0, 7, 7));
114 | rTree.add(new Rect2d(0, 0, 7, 7));
115 |
116 | rTree.add(new Rect2d(0, 0, 8, 8));
117 | rTree.add(new Rect2d(0, 0, 9, 9));
118 | rTree.add(new Rect2d(0, 1, 2, 2));
119 | rTree.add(new Rect2d(0, 1, 3, 3));
120 | rTree.add(new Rect2d(0, 1, 4, 4));
121 |
122 | rTree.add(new Rect2d(0, 1, 4, 4));
123 | rTree.add(new Rect2d(0, 1, 5, 5));
124 |
125 | // 17 entries guarantees *at least* 2 splits when max leaf size is 8
126 | final int expectedEntryCount = 17;
127 |
128 | final Stats stats = rTree.collectStats();
129 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + expectedEntryCount + " actual: " + stats.getEntryCount(), expectedEntryCount, stats.getEntryCount());
130 | }
131 |
132 | /**
133 | * Adds many random entries to trees of different types and confirms that
134 | * no entries are lost during insert/split.
135 | */
136 | @Test
137 | public void randomEntryTest() {
138 |
139 | final int entryCount = 50000;
140 | final Rect2d[] rects = RTreeTest.generateRandomRects(entryCount);
141 |
142 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
143 | for (int i = 0; i < rects.length; i++) {
144 | rTree.add(rects[i]);
145 | }
146 |
147 | final Stats stats = rTree.collectStats();
148 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + entryCount + " actual: " + stats.getEntryCount(), entryCount, stats.getEntryCount());
149 | stats.print(System.out);
150 | }
151 |
152 | /**
153 | * This test previously caused a StackOverflowException.
154 | * It has since been fixed, but keeping the test to ensure
155 | * it doesn't happen again.
156 | */
157 | @Test
158 | public void causeLinearSplitOverflow() {
159 | final RTree rTree = RTreeTest.createRect2DTree(2, 8, TYPE);
160 | final Random rand = new Random(13);
161 | for (int i = 0; i < 500; i++) {
162 | final int x1 = rand.nextInt(10);
163 | final int y1 = rand.nextInt(10);
164 | final int x2 = x1 + rand.nextInt(200);
165 | final int y2 = y1 + rand.nextInt(200);
166 |
167 | rTree.add(new Rect2d(x1, y1, x2, y2));
168 | }
169 | final Stats stats = rTree.collectStats();
170 | stats.print(System.out);
171 | }
172 |
173 |
174 | @Test
175 | public void causeLinearSplitNiceDist() {
176 |
177 | final RTree rTree = RTreeTest.createRect2DTree(2, 8, TYPE);
178 | final Random rand = new Random(13);
179 | for (int i = 0; i < 500; i++) {
180 | final int x1 = rand.nextInt(250);
181 | final int y1 = rand.nextInt(250);
182 | final int x2 = x1 + rand.nextInt(10);
183 | final int y2 = y1 + rand.nextInt(10);
184 |
185 | rTree.add(new Rect2d(x1, y1, x2, y2));
186 | }
187 | final Stats stats = rTree.collectStats();
188 | stats.print(System.out);
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/QuadraticSplitLeafTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Rect2d;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | import java.util.Random;
28 |
29 | /**
30 | * Created by jcovert on 6/12/15.
31 | */
32 | public class QuadraticSplitLeafTest {
33 |
34 | private static final RTree.Split TYPE = RTree.Split.QUADRATIC;
35 |
36 | /**
37 | * Adds enough entries to force a single split and confirms that
38 | * no entries are lost.
39 | */
40 | @Test
41 | public void basicSplitTest() {
42 |
43 | RTree rTree = RTreeTest.createRect2DTree(TYPE);
44 | rTree.add(new Rect2d(0, 0, 1, 1));
45 | rTree.add(new Rect2d(1, 1, 2, 2));
46 | rTree.add(new Rect2d(2, 2, 3, 3));
47 | rTree.add(new Rect2d(3, 3, 4, 4));
48 | rTree.add(new Rect2d(4, 4, 5, 5));
49 | rTree.add(new Rect2d(5, 5, 6, 6));
50 | rTree.add(new Rect2d(6, 6, 7, 7));
51 | rTree.add(new Rect2d(7, 7, 8, 8));
52 | // 9 entries guarantees a split
53 | rTree.add(new Rect2d(8, 8, 9, 9));
54 |
55 | Stats stats = rTree.collectStats();
56 | Assert.assertTrue("Unexpected max depth after basic split", stats.getMaxDepth() == 1);
57 | Assert.assertTrue("Unexpected number of branches after basic split", stats.getBranchCount() == 1);
58 | Assert.assertTrue("Unexpected number of leaves after basic split", stats.getLeafCount() == 2);
59 | Assert.assertTrue("Unexpected number of entries per leaf after basic split", stats.getEntriesPerLeaf() == 4.5);
60 | }
61 |
62 | @Test
63 | public void splitCorrectnessTest() {
64 |
65 | RTree rTree = RTreeTest.createRect2DTree(2, 4, TYPE);
66 | rTree.add(new Rect2d(0, 0, 3, 3));
67 | rTree.add(new Rect2d(1, 1, 2, 2));
68 | rTree.add(new Rect2d(2, 2, 4, 4));
69 | rTree.add(new Rect2d(4, 0, 5, 1));
70 | // 5 entrees guarantees a split
71 | rTree.add(new Rect2d(0, 2, 1, 4));
72 |
73 | Branch root = (Branch) rTree.getRoot();
74 | Node[] children = root.getChildren();
75 | int childCount = 0;
76 | for(Node c : children) {
77 | if (c != null) {
78 | childCount++;
79 | }
80 | }
81 | Assert.assertEquals("Expected different number of children after split", 2, childCount);
82 |
83 | Node child1 = children[0];
84 | Rect2d child1Mbr = (Rect2d) child1.getBound();
85 | Rect2d expectedChild1Mbr = new Rect2d(0, 0, 4, 4);
86 | Assert.assertEquals("Child 1 size incorrect after split", 4, child1.size());
87 | Assert.assertEquals("Child 1 mbr incorrect after split", expectedChild1Mbr, child1Mbr);
88 |
89 | Node child2 = children[1];
90 | Rect2d child2Mbr = (Rect2d) child2.getBound();
91 | Rect2d expectedChild2Mbr = new Rect2d(4, 0, 5, 1);
92 | Assert.assertEquals("Child 2 size incorrect after split", 1, child2.size());
93 | Assert.assertEquals("Child 2 mbr incorrect after split", expectedChild2Mbr, child2Mbr);
94 | }
95 |
96 | /**
97 | * Adds several overlapping rectangles and confirms that no entries
98 | * are lost during insert/split.
99 | */
100 | @Test
101 | public void overlappingEntryTest() {
102 |
103 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
104 | rTree.add(new Rect2d(0, 0, 1, 1));
105 | rTree.add(new Rect2d(0, 0, 2, 2));
106 | rTree.add(new Rect2d(0, 0, 2, 2));
107 | rTree.add(new Rect2d(0, 0, 3, 3));
108 | rTree.add(new Rect2d(0, 0, 3, 3));
109 |
110 | rTree.add(new Rect2d(0, 0, 4, 4));
111 | rTree.add(new Rect2d(0, 0, 5, 5));
112 | rTree.add(new Rect2d(0, 0, 6, 6));
113 | rTree.add(new Rect2d(0, 0, 7, 7));
114 | rTree.add(new Rect2d(0, 0, 7, 7));
115 |
116 | rTree.add(new Rect2d(0, 0, 8, 8));
117 | rTree.add(new Rect2d(0, 0, 9, 9));
118 | rTree.add(new Rect2d(0, 1, 2, 2));
119 | rTree.add(new Rect2d(0, 1, 3, 3));
120 | rTree.add(new Rect2d(0, 1, 4, 4));
121 |
122 | rTree.add(new Rect2d(0, 1, 4, 4));
123 | rTree.add(new Rect2d(0, 1, 5, 5));
124 |
125 | // 17 entries guarantees *at least* 2 splits when max leaf size is 8
126 | final int expectedEntryCount = 17;
127 |
128 | final Stats stats = rTree.collectStats();
129 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + expectedEntryCount + " actual: " + stats.getEntryCount(), expectedEntryCount, stats.getEntryCount());
130 | }
131 |
132 | /**
133 | * Adds many random entries to trees of different types and confirms that
134 | * no entries are lost during insertion (and split).
135 | */
136 | @Test
137 | public void randomEntryTest() {
138 |
139 | final int entryCount = 50000;
140 | final Rect2d[] rects = RTreeTest.generateRandomRects(entryCount);
141 |
142 | final RTree rTree = RTreeTest.createRect2DTree(TYPE);
143 | for (int i = 0; i < rects.length; i++) {
144 | rTree.add(rects[i]);
145 | }
146 |
147 | final Stats stats = rTree.collectStats();
148 | Assert.assertEquals("Unexpected number of entries in " + TYPE + " split tree: " + stats.getEntryCount() + " entries - expected: " + entryCount + " actual: " + stats.getEntryCount(), entryCount, stats.getEntryCount());
149 | stats.print(System.out);
150 | }
151 |
152 |
153 | /**
154 | * This test previously caused a StackOverflowException on LINEAR leaf.
155 | * It has since been fixed, but keeping the test here to ensure this leaf type
156 | * never falls victim to the same issue.
157 | */
158 | @Test
159 | public void causeLinearSplitOverflow() {
160 | final RTree rTree = RTreeTest.createRect2DTree(2, 8, TYPE);
161 | final Random rand = new Random(13);
162 | for (int i = 0; i < 500; i++) {
163 | final int x1 = rand.nextInt(10);
164 | final int y1 = rand.nextInt(10);
165 | final int x2 = x1 + rand.nextInt(200);
166 | final int y2 = y1 + rand.nextInt(200);
167 |
168 | rTree.add(new Rect2d(x1, y1, x2, y2));
169 | }
170 | final Stats stats = rTree.collectStats();
171 | stats.print(System.out);
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/RTreeTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Point2d;
24 | import com.conversantmedia.util.collection.geometry.Rect2d;
25 | import org.junit.Assert;
26 | import org.junit.Ignore;
27 | import org.junit.Test;
28 |
29 | import java.util.Random;
30 | import java.util.concurrent.atomic.AtomicInteger;
31 | import java.util.List;
32 | import java.util.ArrayList;
33 |
34 | /**
35 | * Created by jcairns on 4/30/15.
36 | */
37 | public class RTreeTest {
38 |
39 | @Test
40 | public void pointSearchTest() {
41 |
42 | final RTree pTree = new RTree<>(new Point2d.Builder(), 2, 8, RTree.Split.AXIAL);
43 |
44 | for(int i=0; i<10; i++) {
45 | pTree.add(new Point2d(i, i));
46 | }
47 |
48 | final Rect2d rect = new Rect2d(new Point2d(2,2), new Point2d(8,8));
49 | final Point2d[] result = new Point2d[10];
50 |
51 | final int n = pTree.search(rect, result);
52 | Assert.assertEquals(7, n);
53 |
54 | for(int i=0; i= 2);
56 | Assert.assertTrue(result[i].getCoord(Point2d.X) <= 8);
57 | Assert.assertTrue(result[i].getCoord(Point2d.Y) >= 2);
58 | Assert.assertTrue(result[i].getCoord(Point2d.Y) <= 8);
59 | }
60 | }
61 |
62 | @Test
63 | public void pointCollectionSearchTest() {
64 |
65 | final RTree pTree = new RTree<>(new Point2d.Builder(), 2, 8, RTree.Split.AXIAL);
66 |
67 | for(int i=0; i<10; i++) {
68 | pTree.add(new Point2d(i, i));
69 | }
70 |
71 | final Rect2d rect = new Rect2d(new Point2d(2,2), new Point2d(8,8));
72 |
73 | final List result = new ArrayList<>();
74 |
75 | pTree.search(rect, result);
76 |
77 | final int n = result.size();
78 |
79 | Assert.assertEquals(7, n);
80 |
81 | for(int i=0; i= 2);
83 | Assert.assertTrue(result.get(i).getCoord(Point2d.X) <= 8);
84 | Assert.assertTrue(result.get(i).getCoord(Point2d.Y) >= 2);
85 | Assert.assertTrue(result.get(i).getCoord(Point2d.Y) <= 8);
86 | }
87 | }
88 |
89 | /**
90 | * Use an small bounding box to ensure that only expected rectangles are returned.
91 | * Verifies the count returned from search AND the number of rectangles results.
92 | */
93 | @Test
94 | public void rect2DSearchTest() {
95 |
96 | final int entryCount = 20;
97 |
98 | for (RTree.Split type : RTree.Split.values()) {
99 | RTree rTree = createRect2DTree(2, 8, type);
100 | for (int i = 0; i < entryCount; i++) {
101 | rTree.add(new Rect2d(i, i, i+3, i+3));
102 | }
103 |
104 | final Rect2d searchRect = new Rect2d(5, 5, 10, 10);
105 | Rect2d[] results = new Rect2d[entryCount];
106 |
107 | final int foundCount = rTree.search(searchRect, results);
108 | int resultCount = 0;
109 | for(int i = 0; i < results.length; i++) {
110 | if(results[i] != null) {
111 | resultCount++;
112 | }
113 | }
114 |
115 | final int expectedCount = 3;
116 | Assert.assertEquals("[" + type + "] Search returned incorrect search result count - expected: " + expectedCount + " actual: " + foundCount, expectedCount, foundCount);
117 | Assert.assertEquals("[" + type + "] Search returned incorrect number of rectangles - expected: " + expectedCount + " actual: " + resultCount, expectedCount, resultCount);
118 |
119 | // If the order of nodes in the tree changes, this test may fail while returning the correct results.
120 | for (int i = 0; i < resultCount; i++) {
121 | Assert.assertTrue("Unexpected result found", RTree.isEqual(results[i].getMin().getCoord(Point2d.X), i + 5) &&
122 | RTree.isEqual(results[i].getMin().getCoord(Point2d.Y), i + 5) &&
123 | RTree.isEqual(results[i].getMax().getCoord(Point2d.X), i + 8) &&
124 | RTree.isEqual(results[i].getMax().getCoord(Point2d.Y), i + 8));
125 | }
126 | }
127 | }
128 |
129 | /**
130 | * Use an small bounding box to ensure that only expected rectangles are returned.
131 | * Verifies the count returned from search AND the number of rectangles results.
132 | */
133 | @Test
134 | public void rect2DIntersectTest() {
135 |
136 | final int entryCount = 20;
137 |
138 | for (RTree.Split type : RTree.Split.values()) {
139 | RTree rTree = createRect2DTree(2, 8, type);
140 | for (int i = 0; i < entryCount; i++) {
141 | rTree.add(new Rect2d(i, i, i+3, i+3));
142 | }
143 |
144 | final Rect2d searchRect = new Rect2d(5, 5, 10, 10);
145 | Rect2d[] results = new Rect2d[entryCount];
146 |
147 | final int foundCount = rTree.intersects(searchRect, results);
148 | int resultCount = 0;
149 | for(int i = 0; i < results.length; i++) {
150 | if(results[i] != null) {
151 | resultCount++;
152 | }
153 | }
154 |
155 | final int expectedCount = 9;
156 | Assert.assertEquals("[" + type + "] Search returned incorrect search result count - expected: " + expectedCount + " actual: " + foundCount, expectedCount, foundCount);
157 | Assert.assertEquals("[" + type + "] Search returned incorrect number of rectangles - expected: " + expectedCount + " actual: " + resultCount, expectedCount, resultCount);
158 |
159 | // If the order of nodes in the tree changes, this test may fail while returning the correct results.
160 | for (int i = 0; i < resultCount; i++) {
161 | Assert.assertTrue("Unexpected result found", RTree.isEqual(results[i].getMin().getCoord(Point2d.X), i + 2) &&
162 | RTree.isEqual(results[i].getMin().getCoord(Point2d.Y), i + 2) &&
163 | RTree.isEqual(results[i].getMax().getCoord(Point2d.X), i + 5) &&
164 | RTree.isEqual(results[i].getMax().getCoord(Point2d.Y), i + 5));
165 | }
166 | }
167 | }
168 |
169 |
170 |
171 | /**
172 | * Use an enormous bounding box to ensure that every rectangle is returned.
173 | * Verifies the count returned from search AND the number of rectangles results.
174 | */
175 | @Test
176 | public void rect2DSearchAllTest() {
177 |
178 | final int entryCount = 1000;
179 | final Rect2d[] rects = generateRandomRects(entryCount);
180 |
181 | for (RTree.Split type : RTree.Split.values()) {
182 | RTree rTree = createRect2DTree(2, 8, type);
183 | for (int i = 0; i < rects.length; i++) {
184 | rTree.add(rects[i]);
185 | }
186 |
187 | final Rect2d searchRect = new Rect2d(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
188 | Rect2d[] results = new Rect2d[entryCount];
189 |
190 | final int foundCount = rTree.search(searchRect, results);
191 | int resultCount = 0;
192 | for(int i = 0; i < results.length; i++) {
193 | if(results[i] != null) {
194 | resultCount++;
195 | }
196 | }
197 |
198 | final AtomicInteger visitCount = new AtomicInteger();
199 | rTree.search(searchRect, (n) -> {visitCount.incrementAndGet();});
200 | Assert.assertEquals(entryCount, visitCount.get());
201 |
202 | final int expectedCount = entryCount;
203 | Assert.assertEquals("[" + type + "] Search returned incorrect search result count - expected: " + expectedCount + " actual: " + foundCount, expectedCount, foundCount);
204 | Assert.assertEquals("[" + type + "] Search returned incorrect number of rectangles - expected: " + expectedCount + " actual: " + resultCount, expectedCount, resultCount);
205 | }
206 | }
207 |
208 | /**
209 | * Collect stats making the structure of trees of each split type
210 | * more visible.
211 | */
212 | @Ignore
213 | // This test ignored because output needs to be manually evaluated.
214 | public void treeStructureStatsTest() {
215 |
216 | final int entryCount = 50_000;
217 |
218 | final Rect2d[] rects = generateRandomRects(entryCount);
219 | for (RTree.Split type : RTree.Split.values()) {
220 | RTree rTree = createRect2DTree(2, 8, type);
221 | for (int i = 0; i < rects.length; i++) {
222 | rTree.add(rects[i]);
223 | }
224 |
225 | Stats stats = rTree.collectStats();
226 | stats.print(System.out);
227 | }
228 | }
229 |
230 | /**
231 | * Do a search and collect stats on how many nodes we hit and how many
232 | * bounding boxes we had to evaluate to get all the results.
233 | *
234 | * Preliminary findings:
235 | * - Evals for QUADRATIC tree increases with size of the search bounding box.
236 | * - QUADRATIC seems to be ideal for small search bounding boxes.
237 | */
238 | @Ignore
239 | // This test ignored because output needs to be manually evaluated.
240 | public void treeSearchStatsTest() {
241 |
242 | final int entryCount = 5000;
243 |
244 | final Rect2d[] rects = generateRandomRects(entryCount);
245 |
246 | for(int j=0; j<6; j++) {
247 | for (RTree.Split type : RTree.Split.values()) {
248 | RTree rTree = createRect2DTree(2, 12, type);
249 | for (int i = 0; i < rects.length; i++) {
250 | rTree.add(rects[i]);
251 | }
252 |
253 | rTree.instrumentTree();
254 |
255 | final Rect2d searchRect = new Rect2d(100, 100, 120, 120);
256 | Rect2d[] results = new Rect2d[entryCount];
257 | final long start = System.nanoTime();
258 | int foundCount = rTree.search(searchRect, results);
259 | final long end = System.nanoTime() - start;
260 | CounterNode root = (CounterNode) rTree.getRoot();
261 |
262 | System.out.println("[" + type + "] searched " + root.searchCount + " nodes, returning " + foundCount + " entries");
263 | System.out.println("[" + type + "] evaluated " + root.bboxEvalCount + " b-boxes, returning " + foundCount + " entries");
264 |
265 | System.out.println("Run was " + end / 1000 + " us");
266 | }
267 | }
268 | }
269 |
270 | @Test
271 | public void treeContainsTest() {
272 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
273 |
274 | final Rect2d[] rects = new Rect2d[5];
275 | for (int i = 0; i < rects.length; i++) {
276 | rects[i] = new Rect2d(i, i, i + 1, i + 1);
277 | rTree.add(rects[i]);
278 | }
279 |
280 | for (int i = 0; i < rects.length; i++) {
281 | Assert.assertTrue(rTree.contains(rects[i]));
282 | }
283 | }
284 |
285 |
286 | @Test
287 | public void treeRemovalTest5Entries() {
288 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
289 |
290 | final Rect2d[] rects = new Rect2d[5];
291 | for(int i = 0; i < rects.length; i++){
292 | rects[i] = new Rect2d(i, i, i+1, i+1);
293 | rTree.add(rects[i]);
294 | }
295 |
296 | for(int i = 1; i < rects.length; i++) {
297 | rTree.remove(rects[i]);
298 | Assert.assertEquals(rects.length-i, rTree.getEntryCount());
299 | }
300 |
301 | Assert.assertTrue("Missing hyperRect that should be found " + rects[0], rTree.contains(rects[0]));
302 |
303 | for(int i = 1; i < rects.length; i++) {
304 | Assert.assertFalse("Found hyperRect that should have been removed on search " + rects[i], rTree.contains(rects[i]));
305 | }
306 |
307 | final Rect2d hr = new Rect2d(0,0,5,5);
308 | rTree.add(hr);
309 | Assert.assertTrue(rTree.contains(hr));
310 | Assert.assertTrue("Found hyperRect that should have been removed on search", rTree.getEntryCount() != 0);
311 | }
312 |
313 | @Test
314 | public void treeGetEntryCount() {
315 |
316 | final int NENTRY = 500;
317 |
318 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
319 |
320 | for(int i = 0; i < NENTRY; i++){
321 | final Rect2d rect = new Rect2d(i, i, i+1, i+1);
322 | rTree.add(rect);
323 | }
324 |
325 | Assert.assertEquals(NENTRY, rTree.getEntryCount());
326 | }
327 |
328 |
329 | @Test
330 | public void treeRemovalTestDuplicates() {
331 |
332 | final int NENTRY = 50;
333 |
334 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
335 |
336 | final Rect2d[] rect = new Rect2d[2];
337 | for(int i = 0; i < rect.length; i++){
338 | rect[i] = new Rect2d(i, i, i+1, i+1);
339 | rTree.add(rect[i]);
340 | }
341 |
342 | for(int i = 0; i< NENTRY; i++) {
343 | rTree.add(rect[1]);
344 | }
345 |
346 | Assert.assertEquals(NENTRY+2, rTree.getEntryCount());
347 |
348 | for(int i = 0; i < rect.length; i++) {
349 | rTree.remove(rect[i]);
350 | }
351 |
352 | for(int i = 0; i < rect.length; i++) {
353 | Assert.assertFalse("Found hyperRect that should have been removed " + rect[i], rTree.contains(rect[i]));
354 | }
355 | }
356 |
357 | @Test
358 | public void treeRemovalTest1000Entries() {
359 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
360 |
361 | final Rect2d[] rect = new Rect2d[1000];
362 | for(int i = 0; i < rect.length; i++){
363 | rect[i] = new Rect2d(i, i, i+1, i+1);
364 | rTree.add(rect[i]);
365 | }
366 |
367 | for(int i = 0; i < rect.length; i++) {
368 | rTree.remove(rect[i]);
369 | }
370 |
371 | for(int i = 0; i < rect.length; i++) {
372 | Assert.assertFalse("Found hyperRect that should have been removed" + rect[i], rTree.contains(rect[i]));
373 | }
374 |
375 | Assert.assertFalse("Found hyperRect that should have been removed on search ", rTree.getEntryCount() > 0);
376 | }
377 |
378 | @Test
379 | public void treeSingleRemovalTest() {
380 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
381 |
382 | Rect2d rect = new Rect2d(0,0,2,2);
383 | rTree.add(rect);
384 | Assert.assertTrue("Did not add HyperRect to Tree", rTree.getEntryCount() > 0);
385 | rTree.remove(rect);
386 | Assert.assertTrue("Did not remove HyperRect from Tree", rTree.getEntryCount() == 0);
387 | rTree.add(rect);
388 | Assert.assertTrue("Tree nulled out and could not add HyperRect back in", rTree.getEntryCount() > 0);
389 | }
390 |
391 | @Ignore
392 | // This test ignored because output needs to be manually evaluated.
393 | public void treeRemoveAndRebalanceTest() {
394 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
395 |
396 | Rect2d[] rect = new Rect2d[65];
397 | for(int i = 0; i < rect.length; i++){
398 | if(i < 4){ rect[i] = new Rect2d(0,0,1,1); }
399 | else if(i < 8) { rect[i] = new Rect2d(2, 2, 4, 4); }
400 | else if(i < 12) { rect[i] = new Rect2d(4,4,5,5); }
401 | else if(i < 16) { rect[i] = new Rect2d(5,5,6,6); }
402 | else if(i < 20) { rect[i] = new Rect2d(6,6,7,7); }
403 | else if(i < 24) { rect[i] = new Rect2d(7,7,8,8); }
404 | else if(i < 28) { rect[i] = new Rect2d(8,8,9,9); }
405 | else if(i < 32) { rect[i] = new Rect2d(9,9,10,10); }
406 | else if(i < 36) { rect[i] = new Rect2d(2,2,4,4); }
407 | else if(i < 40) { rect[i] = new Rect2d(4,4,5,5); }
408 | else if(i < 44) { rect[i] = new Rect2d(5,5,6,6); }
409 | else if(i < 48) { rect[i] = new Rect2d(6,6,7,7); }
410 | else if(i < 52) { rect[i] = new Rect2d(7,7,8,8); }
411 | else if(i < 56) { rect[i] = new Rect2d(8,8,9,9); }
412 | else if(i < 60) { rect[i] = new Rect2d(9,9,10,10); }
413 | else if(i < 65) { rect[i] = new Rect2d(1,1,2,2); }
414 | }
415 | for(int i = 0; i < rect.length; i++){
416 | rTree.add(rect[i]);
417 | }
418 | Stats stat = rTree.collectStats();
419 | stat.print(System.out);
420 | for(int i = 0; i < 5; i++){
421 | rTree.remove(rect[64]);
422 | }
423 | Stats stat2 = rTree.collectStats();
424 | stat2.print(System.out);
425 | }
426 |
427 | @Test
428 | public void treeUpdateTest() {
429 | final RTree rTree = createRect2DTree(RTree.Split.QUADRATIC);
430 |
431 | Rect2d rect = new Rect2d(0, 1, 2, 3);
432 | rTree.add(rect);
433 | Rect2d oldRect = new Rect2d(0,1,2,3);
434 | Rect2d newRect = new Rect2d(1,2,3,4);
435 | rTree.update(oldRect, newRect);
436 | Rect2d[] results = new Rect2d[2];
437 | final int num = rTree.search(newRect, results);
438 | Assert.assertTrue("Did not find the updated HyperRect", num == 1);
439 | System.out.print(results[0]);
440 | }
441 |
442 | @Test
443 | public void testAddsubtreeWithSideTree() {
444 | final RTree rTree = createRect2DTree(3, 6, RTree.Split.QUADRATIC);
445 |
446 | final Rect2d search;
447 |
448 | rTree.add(new Rect2d(2, 2, 4, 4));
449 | rTree.add(search = new Rect2d(5, 2, 6, 3));
450 |
451 | // now make sure root node is a branch
452 | for(int i=0; i<5; i++) {
453 | rTree.add(new Rect2d(3.0 - 1.0/(10.0+i),3.0 - 1.0/(10.0+i), 3.0 + 1.0/(10.0+i),3.0 + 1.0/(10.0+i)));
454 | }
455 |
456 | // add subtree/child on first rectangle - fully contained
457 | rTree.add(new Rect2d(2.5, 2.5, 3.5, 3.5));
458 |
459 | Assert.assertEquals(8, rTree.getEntryCount());
460 |
461 | final AtomicInteger hitCount = new AtomicInteger();
462 | // but 5, 2, 6, 3 must still be found!
463 | rTree.search(search, (closure) -> { hitCount.incrementAndGet();});
464 |
465 | Assert.assertEquals(1, hitCount.get());
466 |
467 | }
468 |
469 | /**
470 | * Generate 'count' random rectangles with fixed ranges.
471 | *
472 | * @param count - number of rectangles to generate
473 | * @return array of generated rectangles
474 | */
475 | @Ignore
476 | static Rect2d[] generateRandomRects(int count) {
477 | final Random rand = new Random(13);
478 |
479 | // changing these values changes the rectangle sizes and consequently the distribution density
480 | final int minX = 500;
481 | final int minY = 500;
482 | final int maxXRange = 25;
483 | final int maxYRange = 25;
484 |
485 | final double hitProb = 1.0 * count * maxXRange * maxYRange / (minX * minY);
486 |
487 | final Rect2d[] rects = new Rect2d[count];
488 | for (int i = 0; i < count; i++) {
489 | final int x1 = rand.nextInt(minX);
490 | final int y1 = rand.nextInt(minY);
491 | final int x2 = x1 + rand.nextInt(maxXRange);
492 | final int y2 = y1 + rand.nextInt(maxYRange);
493 | rects[i] = new Rect2d(x1, y1, x2, y2);
494 | }
495 |
496 | return rects;
497 | }
498 |
499 | /**
500 | * Create a tree capable of holding rectangles with default minM (2) and maxM (8) values.
501 | *
502 | * @param splitType - type of leaf to use (affects how full nodes get split)
503 | * @return tree
504 | */
505 | @Ignore
506 | static RTree createRect2DTree(RTree.Split splitType) {
507 | return createRect2DTree(2, 8, splitType);
508 | }
509 |
510 | /**
511 | * Create a tree capable of holding rectangles with specified m and M values.
512 | *
513 | * @param minM - minimum number of entries in each leaf
514 | * @param maxM - maximum number of entries in each leaf
515 | * @param splitType - type of leaf to use (affects how full nodes get split)
516 | * @return tree
517 | */
518 | @Ignore
519 | static RTree createRect2DTree(int minM, int maxM, RTree.Split splitType) {
520 | return new RTree<>(new Rect2d.Builder(), minM, maxM, splitType);
521 | }
522 | }
523 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/Rect1DTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2018, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * John Cairns © 2018
9 | * ~~
10 | * Licensed under the Apache License, Version 2.0 (the "License");
11 | * you may not use this file except in compliance with the License.
12 | * You may obtain a copy of the License at
13 | *
14 | * http://www.apache.org/licenses/LICENSE-2.0
15 | *
16 | * Unless required by applicable law or agreed to in writing, software
17 | * distributed under the License is distributed on an "AS IS" BASIS,
18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 | * See the License for the specific language governing permissions and
20 | * limitations under the License.
21 | * #L%
22 | */
23 |
24 |
25 | import com.conversantmedia.util.collection.geometry.Range1d;
26 | import org.junit.Assert;
27 | import org.junit.Test;
28 |
29 | /**
30 | * Created by jcairns on 7/18/18.
31 | */
32 | public class Rect1DTest {
33 |
34 | @Test
35 | public void centroidTest() {
36 | Range1d rect = new Range1d(0D, 1D);
37 | Assert.assertTrue(RTree.isEqual(.5D, rect.getCentroid().getCoord(0)));
38 | }
39 |
40 | @Test
41 | public void mbrTest() {
42 | Range1d r1 = new Range1d(0, 5);
43 | Range1d r2 = new Range1d(4, 7);
44 | Range1d r3 = new Range1d(-4, 6);
45 |
46 | Range1d mbr = (Range1d)r1.getMbr(r2);
47 | mbr = (Range1d)mbr.getMbr(r3);
48 |
49 | Assert.assertTrue(RTree.isEqual(-4, mbr.getMin().getCoord(0)));
50 | Assert.assertTrue(RTree.isEqual(7, mbr.getMax().getCoord(0)));
51 | }
52 |
53 | @Test
54 | public void checkContains() {
55 |
56 | Range1d r1 = new Range1d(0, 5);
57 |
58 | Range1d r2 = new Range1d(2, 3);
59 |
60 | Range1d r3 = new Range1d(3, 10);
61 |
62 | Assert.assertTrue(r1.contains(r2));
63 | Assert.assertFalse(r2.contains(r1));
64 | Assert.assertFalse(r1.contains(r3));
65 |
66 | }
67 |
68 | @Test
69 | public void checkIntersects() {
70 |
71 | Range1d r1 = new Range1d(0, 5);
72 |
73 | Range1d r2 = new Range1d(2, 3);
74 |
75 | Range1d r3 = new Range1d(3, 10);
76 |
77 | Assert.assertTrue(r1.intersects(r2));
78 | Assert.assertTrue(r2.intersects(r1));
79 | Assert.assertTrue(r1.intersects(r3));
80 |
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/Rect2DTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Rect2d;
24 | import org.junit.Test;
25 | import org.junit.Assert;
26 |
27 | /**
28 | * Created by jcovert on 6/16/15.
29 | */
30 | public class Rect2DTest {
31 |
32 | @Test
33 | public void centroidTest() {
34 |
35 | Rect2d rect = new Rect2d(0, 0, 4, 3);
36 |
37 | HyperPoint centroid = rect.getCentroid();
38 | double x = centroid.getCoord(0);
39 | double y = centroid.getCoord(1);
40 | Assert.assertTrue("Bad X-coord of centroid - expected " + 2.0 + " but was " + x, RTree.isEqual(x, 2.0d));
41 | Assert.assertTrue("Bad Y-coord of centroid - expected " + 1.5 + " but was " + y, RTree.isEqual(y, 1.5d));
42 | }
43 |
44 | @Test
45 | public void mbrTest() {
46 |
47 | Rect2d rect = new Rect2d(0, 0, 4, 3);
48 |
49 | // shouldn't effect MBR
50 | Rect2d rectInside = new Rect2d(0, 0, 1, 1);
51 | HyperRect mbr = rect.getMbr(rectInside);
52 | double expectedMinX = rect.getMin().getCoord(0);
53 | double expectedMinY = rect.getMin().getCoord(1);
54 | double expectedMaxX = rect.getMax().getCoord(0);
55 | double expectedMaxY = rect.getMax().getCoord(1);
56 | double actualMinX = mbr.getMin().getCoord(0);
57 | double actualMinY = mbr.getMin().getCoord(1);
58 | double actualMaxX = mbr.getMax().getCoord(0);
59 | double actualMaxY = mbr.getMax().getCoord(1);
60 | Assert.assertTrue("Bad minX - Expected: " + expectedMinX + " Actual: " + actualMinX, actualMinX == expectedMinX);
61 | Assert.assertTrue("Bad minY - Expected: " + expectedMinY + " Actual: " + actualMinY, actualMinY == expectedMinY);
62 | Assert.assertTrue("Bad maxX - Expected: " + expectedMaxX + " Actual: " + actualMaxX, actualMaxX == expectedMaxX);
63 | Assert.assertTrue("Bad maxY - Expected: " + expectedMaxY + " Actual: " + actualMaxY, actualMaxY == expectedMaxY);
64 |
65 | // should affect MBR
66 | Rect2d rectOverlap = new Rect2d(3, 1, 5, 4);
67 | mbr = rect.getMbr(rectOverlap);
68 | expectedMinX = 0.0d;
69 | expectedMinY = 0.0d;
70 | expectedMaxX = 5.0d;
71 | expectedMaxY = 4.0d;
72 | actualMinX = mbr.getMin().getCoord(0);
73 | actualMinY = mbr.getMin().getCoord(1);
74 | actualMaxX = mbr.getMax().getCoord(0);
75 | actualMaxY = mbr.getMax().getCoord(1);
76 | Assert.assertTrue("Bad minX - Expected: " + expectedMinX + " Actual: " + actualMinX, actualMinX == expectedMinX);
77 | Assert.assertTrue("Bad minY - Expected: " + expectedMinY + " Actual: " + actualMinY, actualMinY == expectedMinY);
78 | Assert.assertTrue("Bad maxX - Expected: " + expectedMaxX + " Actual: " + actualMaxX, actualMaxX == expectedMaxX);
79 | Assert.assertTrue("Bad maxY - Expected: " + expectedMaxY + " Actual: " + actualMaxY, actualMaxY == expectedMaxY);
80 | }
81 |
82 | @Test
83 | public void rangeTest() {
84 |
85 | Rect2d rect = new Rect2d(0, 0, 4, 3);
86 |
87 | double xRange = rect.getRange(0);
88 | double yRange = rect.getRange(1);
89 | Assert.assertTrue("Bad range in dimension X - expected " + 4.0 + " but was " + xRange, xRange == 4.0d);
90 | Assert.assertTrue("Bad range in dimension Y - expected " + 3.0 + " but was " + yRange, yRange == 3.0d);
91 | }
92 |
93 |
94 | @Test
95 | public void containsTest() {
96 |
97 | Rect2d rect = new Rect2d(0, 0, 4, 3);
98 |
99 | // shares an edge on the outside, not contained
100 | Rect2d rectOutsideNotContained = new Rect2d(4, 2, 5, 3);
101 | Assert.assertTrue("Shares an edge but should not be 'contained'", !rect.contains(rectOutsideNotContained));
102 |
103 | // shares an edge on the inside, not contained
104 | Rect2d rectInsideNotContained = new Rect2d(0, 1, 4, 5);
105 | Assert.assertTrue("Shares an edge but should not be 'contained'", !rect.contains(rectInsideNotContained));
106 |
107 | // shares an edge on the inside, contained
108 | Rect2d rectInsideContained = new Rect2d(0, 1, 1, 2);
109 | Assert.assertTrue("Shares an edge and should be 'contained'", rect.contains(rectInsideContained));
110 |
111 | // intersects
112 | Rect2d rectIntersects = new Rect2d(3, 2, 5, 4);
113 | Assert.assertTrue("Intersects but should not be 'contained'", !rect.contains(rectIntersects));
114 |
115 | // contains
116 | Rect2d rectContained = new Rect2d(1, 1, 2, 2);
117 | Assert.assertTrue("Contains and should be 'contained'", rect.contains(rectContained));
118 |
119 | // does not contain or intersect
120 | Rect2d rectNotContained = new Rect2d(5, 0, 6, 1);
121 | Assert.assertTrue("Does not contain and should not be 'contained'", !rect.contains(rectNotContained));
122 | }
123 |
124 | @Test
125 | public void intersectsTest() {
126 |
127 | Rect2d rect = new Rect2d(0, 0, 4, 3);
128 |
129 | // shares an edge on the outside, intersects
130 | Rect2d rectOutsideIntersects = new Rect2d(4, 2, 5, 3);
131 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectOutsideIntersects));
132 |
133 | // shares an edge on the inside, intersects
134 | Rect2d rectInsideIntersects = new Rect2d(0, 1, 4, 5);
135 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectInsideIntersects));
136 |
137 | // shares an edge on the inside, intersects
138 | Rect2d rectInsideIntersectsContained = new Rect2d(0, 1, 1, 2);
139 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectInsideIntersectsContained));
140 |
141 | // intersects
142 | Rect2d rectIntersects = new Rect2d(3, 2, 5, 4);
143 | Assert.assertTrue("Intersects and should 'intersect'", rect.intersects(rectIntersects));
144 |
145 | // contains
146 | Rect2d rectContained = new Rect2d(1, 1, 2, 2);
147 | Assert.assertTrue("Contains and should 'intersect'", rect.intersects(rectContained));
148 |
149 | // does not contain or intersect
150 | Rect2d rectNotIntersects = new Rect2d(5, 0, 6, 1);
151 | Assert.assertTrue("Does not intersect and should not 'intersect'", !rect.intersects(rectNotIntersects));
152 | }
153 |
154 | @Test
155 | public void costTest() {
156 |
157 | Rect2d rect = new Rect2d(0, 0, 4, 3);
158 | double cost = rect.cost();
159 | Assert.assertTrue("Bad cost - expected " + 12.0 + " but was " + cost, cost == 12.0d);
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/test/java/com/conversantmedia/util/collection/spatial/Rect3DTest.java:
--------------------------------------------------------------------------------
1 | package com.conversantmedia.util.collection.spatial;
2 |
3 | /*
4 | * #%L
5 | * Conversant RTree
6 | * ~~
7 | * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
8 | * ~~
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | * #L%
21 | */
22 |
23 | import com.conversantmedia.util.collection.geometry.Point3d;
24 | import com.conversantmedia.util.collection.geometry.Rect2d;
25 | import com.conversantmedia.util.collection.geometry.Rect3d;
26 | import org.junit.Assert;
27 | import org.junit.Test;
28 |
29 | /**
30 | * Created by jcovert on 6/16/15.
31 | */
32 | public class Rect3DTest {
33 |
34 | @Test
35 | public void centroidTest() {
36 |
37 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 2);
38 |
39 | HyperPoint centroid = rect.getCentroid();
40 | double x = centroid.getCoord(Point3d.X);
41 | double y = centroid.getCoord(Point3d.Y);
42 | double z = centroid.getCoord(Point3d.Z);
43 | Assert.assertTrue("Bad X-coord of centroid - expected " + 2.0 + " but was " + x, RTree.isEqual(x, 2.0d));
44 | Assert.assertTrue("Bad Y-coord of centroid - expected " + 1.5 + " but was " + y, RTree.isEqual(y, 1.5d));
45 | Assert.assertTrue("Bad Z-coord of centroid - expected " + 1.0 + " but was " + y, RTree.isEqual(z, 1.0d));
46 | }
47 |
48 | @Test
49 | public void mbrTest() {
50 |
51 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 2);
52 |
53 | // shouldn't affect MBR
54 | Rect3d rectInside = new Rect3d(0, 0, 0, 1, 1, 1);
55 | HyperRect mbr = rect.getMbr(rectInside);
56 | double expectedMinX = rect.getMin().getCoord(Point3d.X);
57 | double expectedMinY = rect.getMin().getCoord(Point3d.Y);
58 | double expectedMinZ = rect.getMin().getCoord(Point3d.Z);
59 | double expectedMaxX = rect.getMax().getCoord(Point3d.X);
60 | double expectedMaxY = rect.getMax().getCoord(Point3d.Y);
61 | double expectedMaxZ = rect.getMin().getCoord(Point3d.Z);
62 | double actualMinX = mbr.getMin().getCoord(Point3d.X);
63 | double actualMinY = mbr.getMin().getCoord(Point3d.Y);
64 | double actualMinZ = mbr.getMin().getCoord(Point3d.Z);
65 | double actualMaxX = mbr.getMax().getCoord(Point3d.X);
66 | double actualMaxY = mbr.getMax().getCoord(Point3d.Y);
67 | double actualMaxZ = mbr.getMax().getCoord(Point3d.Z);
68 | Assert.assertTrue("Bad minX - Expected: " + expectedMinX + " Actual: " + actualMinX, RTree.isEqual(actualMinX, expectedMinX));
69 | Assert.assertTrue("Bad minY - Expected: " + expectedMinY + " Actual: " + actualMinY, RTree.isEqual(actualMinY, expectedMinY));
70 | Assert.assertTrue("Bad maxX - Expected: " + expectedMaxX + " Actual: " + actualMaxX, RTree.isEqual(actualMaxX, expectedMaxX));
71 | Assert.assertTrue("Bad maxY - Expected: " + expectedMaxY + " Actual: " + actualMaxY, RTree.isEqual(actualMaxY, expectedMaxY));
72 |
73 | // should affect MBR
74 | Rect3d rectOverlap = new Rect3d(3, 1, 3, 5, 4, 4);
75 | mbr = rect.getMbr(rectOverlap);
76 | expectedMinX = 0.0d;
77 | expectedMinY = 0.0d;
78 | expectedMinZ = 0.0d;
79 | expectedMaxX = 5.0d;
80 | expectedMaxY = 4.0d;
81 | expectedMaxZ = 4.0d;
82 | actualMinX = mbr.getMin().getCoord(Point3d.X);
83 | actualMinY = mbr.getMin().getCoord(Point3d.Y);
84 | actualMinZ = mbr.getMin().getCoord(Point3d.Z);
85 | actualMaxX = mbr.getMax().getCoord(Point3d.X);
86 | actualMaxY = mbr.getMax().getCoord(Point3d.Y);
87 | actualMaxZ = mbr.getMax().getCoord(Point3d.Z);
88 | Assert.assertTrue("Bad minX - Expected: " + expectedMinX + " Actual: " + actualMinX, RTree.isEqual(actualMinX, expectedMinX));
89 | Assert.assertTrue("Bad minY - Expected: " + expectedMinY + " Actual: " + actualMinY, RTree.isEqual(actualMinY, expectedMinY));
90 | Assert.assertTrue("Bad maxX - Expected: " + expectedMaxX + " Actual: " + actualMaxX, RTree.isEqual(actualMaxX, expectedMaxX));
91 | Assert.assertTrue("Bad maxY - Expected: " + expectedMaxY + " Actual: " + actualMaxY, RTree.isEqual(actualMaxY, expectedMaxY));
92 | }
93 |
94 | @Test
95 | public void rangeTest() {
96 |
97 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 2);
98 |
99 | double xRange = rect.getRange(Point3d.X);
100 | double yRange = rect.getRange(Point3d.Y);
101 | double zRange = rect.getRange(Point3d.Z);
102 | Assert.assertTrue("Bad range in dimension X - expected " + 4.0 + " but was " + xRange, RTree.isEqual(xRange, 4.0d));
103 | Assert.assertTrue("Bad range in dimension Y - expected " + 3.0 + " but was " + yRange, RTree.isEqual(yRange, 3.0d));
104 | Assert.assertTrue("Bad range in dimension Y - expected " + 2.0 + " but was " + zRange, RTree.isEqual(zRange, 2.0d));
105 | }
106 |
107 |
108 | @Test
109 | public void containsTest() {
110 |
111 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 2);
112 |
113 | // shares an edge on the outside, not contained
114 | Rect3d rectOutsideNotContained = new Rect3d(4, 2, 4, 5, 3, 5);
115 | Assert.assertTrue("Shares an edge but should not be 'contained'", !rect.contains(rectOutsideNotContained));
116 |
117 | // shares an edge on the inside, not contained
118 | Rect3d rectInsideNotContained = new Rect3d(0, 1, 0, 4, 5, 0);
119 | Assert.assertTrue("Shares an edge but should not be 'contained'", !rect.contains(rectInsideNotContained));
120 |
121 | // shares an edge on the inside, contained
122 | Rect3d rectInsideContained = new Rect3d(0, 1, 0, 1, 2, 0);
123 | Assert.assertTrue("Shares an edge and should be 'contained'", rect.contains(rectInsideContained));
124 |
125 | // intersects
126 | Rect3d rectIntersects = new Rect3d(3, 2, 0, 5, 4, 0);
127 | Assert.assertTrue("Intersects but should not be 'contained'", !rect.contains(rectIntersects));
128 |
129 | // contains
130 | Rect3d rectContained = new Rect3d(1, 1, 1, 2, 2, 2);
131 | Assert.assertTrue("Contains and should be 'contained'", rect.contains(rectContained));
132 |
133 | // does not contain or intersect
134 | Rect3d rectNotContained = new Rect3d(5, 0, 0, 6, 1, 0);
135 | Assert.assertTrue("Does not contain and should not be 'contained'", !rect.contains(rectNotContained));
136 | }
137 |
138 | @Test
139 | public void intersectsTest() {
140 |
141 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 0);
142 |
143 | // shares an edge on the outside, intersects
144 | Rect3d rectOutsideIntersects = new Rect3d(4, 2, 0, 5, 3, 0);
145 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectOutsideIntersects));
146 |
147 | // shares an edge on the inside, intersects
148 | Rect3d rectInsideIntersects = new Rect3d(0, 1, 0, 4, 5, 0);
149 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectInsideIntersects));
150 |
151 | // shares an edge on the inside, intersects
152 | Rect3d rectInsideIntersectsContained = new Rect3d(0, 1, 0, 1, 2, 0);
153 | Assert.assertTrue("Shares an edge and should 'intersect'", rect.intersects(rectInsideIntersectsContained));
154 |
155 | // intersects
156 | Rect3d rectIntersects = new Rect3d(3, 2, 0, 5, 4, 0);
157 | Assert.assertTrue("Intersects and should 'intersect'", rect.intersects(rectIntersects));
158 |
159 | // contains
160 | Rect3d rectContained = new Rect3d(1, 1, 0, 2, 2, 0);
161 | Assert.assertTrue("Contains and should 'intersect'", rect.intersects(rectContained));
162 |
163 | // does not contain or intersect
164 | Rect3d rectNotIntersects = new Rect3d(5, 0, 0, 6, 1, 0);
165 | Assert.assertTrue("Does not intersect and should not 'intersect'", !rect.intersects(rectNotIntersects));
166 | }
167 |
168 | @Test
169 | public void costTest() {
170 |
171 | Rect3d rect = new Rect3d(0, 0, 0, 4, 3, 2);
172 | double cost = rect.cost();
173 | Assert.assertTrue("Bad cost - expected " + 24.0 + " but was " + cost, cost == 24D);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------