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}