001/* 002 * PointRegionQuadTree -- A QuadTree data structure for storing XYPoint objects. 003 * 004 * Copyright (C) 2018, by Joseph A. Huwaldt 005 * All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 020 * MA 02110-1301 USA 021 */ 022package jahuwaldt.util; 023 024import java.util.Collections; 025import java.util.LinkedList; 026import java.util.List; 027 028/** 029 * A QuadTree data structure for storing 2D XYPoint objects. 030 * 031 * <p> Modified by: Joseph A. Huwaldt </p> 032 * 033 * @author Joseph A. Huwaldt, Date: February 5, 2018 034 * @version February 5, 2018 035 * @param <P> The type of XYPoint stored in the QuadTree. 036 */ 037public class PointRegionQuadTree<P extends XYPoint> { 038 039 private QuadNode<P> root = null; 040 041 // A mutable AABB that is used as part of the queryRange() method. 042 private final AxisAlignedBoundingBox RANGE = new AxisAlignedBoundingBox(); 043 044 /** 045 * Create a QuadTree who's upper left coordinate is located at x,y and it's bounding 046 * box is described by the height and width. 047 * 048 * @param xyPoint XYPoint representing the min-X, min-Y corner of the bounding box of 049 * all the 2D points. 050 * @param width Width of the bounding box containing all points 051 * @param height Height of the bounding box containing all points 052 */ 053 public PointRegionQuadTree(P xyPoint, double width, double height) { 054 AxisAlignedBoundingBox aabb = new AxisAlignedBoundingBox(xyPoint, width, height); 055 root = new QuadNode(aabb); 056 } 057 058 /** 059 * Range query of the QuadTree. Return all the points in the quad-tree that fall in 060 * the specified bounding box. 061 * 062 * @param x The minimum X-coordinate of the region to search for points in. 063 * @param y The minimum Y-coordinate of the region to search for points in. 064 * @param width The width of the region to search for points in. 065 * @param height The height of the region to search for points in. 066 * @return A collection of points that fall in the search region. 067 */ 068 public List<P> queryRange(double x, double y, double width, double height) { 069 if (root == null) 070 return Collections.EMPTY_LIST; 071 072 RANGE.set(x, y, width, height); 073 074 List<P> pointsInRange = new LinkedList(); 075 root.queryRange(RANGE, pointsInRange); 076 return pointsInRange; 077 } 078 079 /** 080 * Insert point at X,Y into tree. 081 * 082 * @param xyPoint The 2D point to insert. 083 * @return True if the point was successfully inserted. 084 */ 085 public boolean insert(P xyPoint) { 086 return root.insert(xyPoint); 087 } 088 089 /** 090 * Remove point at X,Y from tree. 091 * 092 * @param xyPoint The 2D point to remove. 093 * @return True of the point was successfully removed from the tree. 094 */ 095 public boolean remove(P xyPoint) { 096 return root.remove(xyPoint); 097 } 098 099 /** 100 * A node for use in a PointRegionQuadTree. 101 * 102 * @param <P> The type of point stored in the node. 103 */ 104 private static class QuadNode<P extends XYPoint> { 105 106 private final AxisAlignedBoundingBox aabb; 107 108 private QuadNode northWest = null; 109 private QuadNode northEast = null; 110 private QuadNode southWest = null; 111 private QuadNode southEast = null; 112 113 // max number of children before sub-dividing 114 private static final int MAX_CAPACITY = 4; 115 116 private List<P> points = new LinkedList(); 117 private int height = 1; 118 119 public QuadNode(AxisAlignedBoundingBox aabb) { 120 this.aabb = aabb; 121 } 122 123 /** 124 * Insert object into tree. 125 * 126 * @param p The 2D point to insert into tree. 127 * @return True if successfully inserted. 128 */ 129 public boolean insert(P p) { 130 // Ignore objects which do not belong in this quad tree 131 if (!aabb.containsPoint(p) || (isLeaf() && points.contains(p))) 132 return false; // object cannot be added 133 134 // If there is space in this quad tree, add the object here 135 if (isLeaf() && points.size() < MAX_CAPACITY) { 136 points.add(p); 137 return true; 138 } 139 140 // Otherwise, we need to subdivide then add the point to whichever node will accept it 141 if (isLeaf()) 142 subdivide(); 143 144 return insertIntoChildren(p); 145 } 146 147 /** 148 * Remove object from tree. 149 * 150 * @param p The 2D Point to remove from tree. 151 * @return True if successfully removed. 152 */ 153 public boolean remove(P p) { 154 // If not in this AABB, don't do anything 155 if (!aabb.containsPoint(p)) 156 return false; 157 158 // If in this AABB and in this node 159 if (points.remove(p)) 160 return true; 161 162 // If this node has children 163 if (!isLeaf()) { 164 // If in this AABB but in a child branch 165 boolean removed = removeFromChildren(p); 166 if (!removed) 167 return false; 168 169 // Try to merge children 170 merge(); 171 172 return true; 173 } 174 175 return false; 176 } 177 178 /** 179 * How many Point objects this node contains. 180 * 181 * @return Number of Point objects this node contains. 182 */ 183 public int size() { 184 return points.size(); 185 } 186 187 private void subdivide() { 188 double h = aabb.height * 0.5; 189 double w = aabb.width * 0.5; 190 191 double x = aabb.getMinX(); 192 double y = aabb.getMinY(); 193 AxisAlignedBoundingBox aabbNW = new AxisAlignedBoundingBox(x,y, w,h); 194 northWest = new QuadNode(aabbNW); 195 northWest.height = height + 1; 196 197 x = aabb.getMinX() + w; 198 y = aabb.getMinY(); 199 AxisAlignedBoundingBox aabbNE = new AxisAlignedBoundingBox(x,y, w,h); 200 northEast = new QuadNode(aabbNE); 201 northEast.height = height + 1; 202 203 x = aabb.getMinX(); 204 y = aabb.getMinY() + h; 205 AxisAlignedBoundingBox aabbSW = new AxisAlignedBoundingBox(x,y, w,h); 206 southWest = new QuadNode(aabbSW); 207 southWest.height = height + 1; 208 209 x = aabb.getMinX() + w; 210 y = aabb.getMinY() + h; 211 AxisAlignedBoundingBox aabbSE = new AxisAlignedBoundingBox(x,y, w,h); 212 southEast = new QuadNode(aabbSE); 213 southEast.height = height + 1; 214 215 // points live in leaf nodes, so distribute 216 for (P p : points) 217 insertIntoChildren(p); 218 points.clear(); 219 } 220 221 private void merge() { 222 // If the children aren't leaves, you cannot merge 223 if (!northWest.isLeaf() || !northEast.isLeaf() || !southWest.isLeaf() || !southEast.isLeaf()) 224 return; 225 226 // Children and leaves, see if you can remove points and merge into this node 227 int nw = northWest.size(); 228 int ne = northEast.size(); 229 int sw = southWest.size(); 230 int se = southEast.size(); 231 int total = nw + ne + sw + se; 232 233 // If all the children's points can be merged into this node 234 if ((size() + total) < MAX_CAPACITY) { 235 this.points.addAll(northWest.points); 236 this.points.addAll(northEast.points); 237 this.points.addAll(southWest.points); 238 this.points.addAll(southEast.points); 239 240 this.northWest = null; 241 this.northEast = null; 242 this.southWest = null; 243 this.southEast = null; 244 } 245 } 246 247 private boolean insertIntoChildren(P p) { 248 // A point can only live in one child. 249 if (northWest.insert(p)) 250 return true; 251 if (northEast.insert(p)) 252 return true; 253 if (southWest.insert(p)) 254 return true; 255 return southEast.insert(p); 256 } 257 258 private boolean removeFromChildren(P p) { 259 // A point can only live in one child. 260 if (northWest.remove(p)) 261 return true; 262 if (northEast.remove(p)) 263 return true; 264 if (southWest.remove(p)) 265 return true; 266 return southEast.remove(p); 267 } 268 269 /** 270 * Find all objects which appear within a range. 271 * 272 * @param range Upper-left and width,height of a axis-aligned bounding 273 * box. 274 * @param pointsInRange The existing list that will be filled with the XYPoint 275 * objects that are inside the bounding box. 276 */ 277 public void queryRange(AxisAlignedBoundingBox range, List<P> pointsInRange) { 278 // Automatically abort if the range does not collide with this quad 279 if (!aabb.intersectsBox(range)) 280 return; 281 282 // If leaf, check objects at this level 283 if (isLeaf()) { 284 for (P xyPoint : points) { 285 if (range.containsPoint(xyPoint)) 286 pointsInRange.add(xyPoint); 287 } 288 return; 289 } 290 291 // Otherwise, add the points from the children 292 northWest.queryRange(range, pointsInRange); 293 northEast.queryRange(range, pointsInRange); 294 southWest.queryRange(range, pointsInRange); 295 southEast.queryRange(range, pointsInRange); 296 } 297 298 /** 299 * Is current node a leaf node. 300 * 301 * @return True if node is a leaf node. 302 */ 303 public boolean isLeaf() { 304 return (northWest == null && northEast == null && southWest == null && southEast == null); 305 } 306 307 } 308 309 /** 310 * An axis-aligned bounding box for use in a quad-tree. 311 */ 312 private static class AxisAlignedBoundingBox { 313 314 private double height = 0; 315 private double width = 0; 316 317 private double minX = 0; 318 private double minY = 0; 319 private double maxX = 0; 320 private double maxY = 0; 321 322 public AxisAlignedBoundingBox() { } 323 324 public AxisAlignedBoundingBox(double minX, double minY, double width, double height) { 325 this.width = width; 326 this.height = height; 327 328 this.minX = minX; 329 this.minY = minY; 330 maxX = minX + width; 331 maxY = minY + height; 332 } 333 334 public AxisAlignedBoundingBox(AxisAlignedBoundingBox aabb, double width, double height) { 335 this.width = width; 336 this.height = height; 337 338 minX = aabb.minX; 339 minY = aabb.minY; 340 maxX = minX + width; 341 maxY = minY + height; 342 } 343 344 public AxisAlignedBoundingBox(XYPoint upperLeft, double width, double height) { 345 this.width = width; 346 this.height = height; 347 348 minX = upperLeft.getX(); 349 minY = upperLeft.getY(); 350 maxX = minX + width; 351 maxY = minY + height; 352 } 353 354 public void set(double minX, double minY, double width, double height) { 355 this.width = width; 356 this.height = height; 357 358 this.minX = minX; 359 this.minY = minY; 360 maxX = minX + width; 361 maxY = minY + height; 362 } 363 364 public double getMinX() { 365 return minX; 366 } 367 368 public double getMinY() { 369 return minY; 370 } 371 372 public double getHeight() { 373 return height; 374 } 375 376 public double getWidth() { 377 return width; 378 } 379 380 public boolean containsPoint(XYPoint p) { 381 double px = p.getX(); 382 double py = p.getY(); 383 if (px >= maxX) 384 return false; 385 if (px < minX) 386 return false; 387 if (py >= maxY) 388 return false; 389 return py >= minY; 390 } 391 392 /** 393 * Is the inputted AxisAlignedBoundingBox completely inside this 394 * AxisAlignedBoundingBox. 395 * 396 * @param b AxisAlignedBoundingBox to test. 397 * @return True if the AxisAlignedBoundingBox is completely inside this 398 * AxisAlignedBoundingBox. 399 */ 400 public boolean insideThis(AxisAlignedBoundingBox b) { 401 return b.minX >= minX && b.maxX <= maxX && b.minY >= minY && b.maxY <= maxY; 402 } 403 404 /** 405 * Is the inputted AxisAlignedBoundingBox intersecting this 406 * AxisAlignedBoundingBox. 407 * 408 * @param b AxisAlignedBoundingBox to test. 409 * @return True if the AxisAlignedBoundingBox is intersecting this 410 * AxisAlignedBoundingBox. 411 */ 412 public boolean intersectsBox(AxisAlignedBoundingBox b) { 413 if (insideThis(b) || b.insideThis(this)) { 414 // INSIDE 415 return true; 416 } 417 418 // OUTSIDE 419 if (maxX < b.minX || minX > b.maxX) 420 return false; 421 return !(maxY < b.minY || minY > b.maxY); 422 } 423 424 } 425 426}