View Javadoc
1   package org.djutils.quadtree;
2   
3   import java.io.Serializable;
4   import java.util.Collection;
5   import java.util.Iterator;
6   import java.util.LinkedHashSet;
7   import java.util.Set;
8   
9   import org.djutils.exceptions.Throw;
10  
11  /**
12   * Quad tree for 2D objects. For now, this implementation needs an ultimate outer bounding box. No part of any 2D string object
13   * may exceed that bounding box. A link to each stored 2D object will be stored in each sub-box that it intersects.
14   * <p>
15   * Copyright (c) 2019-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
17   * </p>
18   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
19   * @param <T> Type of object stored in this quad tree
20   */
21  public class QuadTree<T extends Envelope> implements Collection<T>, Serializable
22  {
23      /** ... */
24      private static final long serialVersionUID = 20200904L;
25  
26      /** Maximum number of payload objects in one cell. */
27      private final int maximumLoad;
28  
29      /** Minimum width and height of a SubTree bounding box. */
30      private final double minimumSize;
31  
32      /** The actual top level quad tree. */
33      private final SubTree<T> tree;
34  
35      /** Count the number of sub trees created. */
36      private int totalSubTrees = 0;
37  
38      /**
39       * Create a new QuadTree object (or a sub-tree).
40       * @param maximumLoad int; number of elements at any level that warrants investigating if the tree can be re-balanced
41       * @param minimumSize double; minimum width or height of a sub tree Rectangle (smaller sub tree are never created)
42       * @param left double; the lowest X-coordinate that is allowed (inclusive)
43       * @param bottom double; the lowest Y-coordinate that is allowed (inclusive)
44       * @param right double; the highest X-coordinate that is allowed (exclusive)
45       * @param top double; the highest Y-coordinate that is allowed (exclusive)
46       */
47      public QuadTree(final int maximumLoad, final double minimumSize, final double left, final double bottom, final double right,
48              final double top)
49      {
50          Throw.when(left >= right, IllegalArgumentException.class, "left (%f) must be less than right (%f)", left, right);
51          Throw.when(bottom >= top, IllegalArgumentException.class, "bottom (%f) must be less than top (%f)", bottom, top);
52          this.maximumLoad = maximumLoad;
53          this.minimumSize = minimumSize;
54          this.tree = new SubTree<T>(this, new Rectangle(left, bottom, right, top));
55      }
56  
57      /**
58       * Return the number of objects at which it is time to try to re-balance.
59       * @return int; the number of objects at which it is time to try to re-balance
60       */
61      public int getMaxLoad()
62      {
63          return this.maximumLoad;
64      }
65  
66      /**
67       * Return the minimum sub-tree rectangle size.
68       * @return double; the minimum sub-tree rectangle size
69       */
70      public double getMinimumSize()
71      {
72          return this.minimumSize;
73      }
74  
75      @Override
76      public int size()
77      {
78          return this.tree.size();
79      }
80  
81      @Override
82      public boolean isEmpty()
83      {
84          return this.tree.size() == 0;
85      }
86  
87      @Override
88      public boolean contains(final Object o)
89      {
90          if (!(o instanceof Envelope))
91          {
92              return false;
93          }
94          @SuppressWarnings("unchecked")
95          T t = (T) o;
96          return this.tree.recursiveContains(new RectangleAndPayload<T>(t.getBoundingRectangle(), t));
97      }
98  
99      @Override
100     public Iterator<T> iterator()
101     {
102         return iterator(this.tree.getBoundingBox());
103     }
104 
105     /**
106      * Find all elements intersecting a given bounding box. This iterator cannot be used to remove elements, but the remove
107      * method can be safely called while the iterator is active.
108      * @param searchArea Rectangle; the bounding box
109      * @return Iterator&lt;T&gt;; iterator that returns all elements that intersect the given bounding box
110      */
111     public Iterator<T> iterator(final Rectangle searchArea)
112     {
113         return collect(searchArea).iterator();
114     }
115 
116     @Override
117     public Object[] toArray()
118     {
119         return collect(this.tree.getBoundingBox()).toArray();
120     }
121 
122     @SuppressWarnings("hiding")
123     @Override
124     public <T> T[] toArray(final T[] a)
125     {
126         return collect(this.tree.getBoundingBox()).toArray(a);
127     }
128 
129     /**
130      * Construct a set containing all payload elements within a specified area.
131      * @param searchArea Rectangle; the search area
132      * @return Set&lt;T&gt;; the set containing all payload elements whose bounding areas intersect the specified area
133      */
134     private Set<T> collect(final Rectangle searchArea)
135     {
136         Iterator<RectangleAndPayload<T>> iterator = this.tree.recursiveCollect(searchArea).iterator();
137         Set<T> result = new LinkedHashSet<>();
138         while (iterator.hasNext())
139         {
140             result.add(iterator.next().getPayload());
141         }
142         return result;
143     }
144 
145     @Override
146     public boolean add(final T e)
147     {
148         return this.tree.add(new RectangleAndPayload<T>(e.getBoundingRectangle(), e));
149     }
150 
151     @Override
152     public boolean remove(final Object o)
153     {
154         if (!(o instanceof Envelope))
155         {
156             return false;
157         }
158         @SuppressWarnings("unchecked")
159         T t = (T) o;
160         return this.tree.remove(new RectangleAndPayload<T>(t.getBoundingRectangle(), t));
161     }
162 
163     @Override
164     public boolean containsAll(final Collection<?> c)
165     {
166         return collect(this.tree.getBoundingBox()).containsAll(c);
167     }
168 
169     @Override
170     public boolean addAll(final Collection<? extends T> c)
171     {
172         boolean result = false;
173         for (T t : c)
174         {
175             result |= add(t);
176         }
177         return result;
178     }
179 
180     @Override
181     public boolean removeAll(final Collection<?> c)
182     {
183         boolean result = false;
184         for (Object o : c)
185         {
186             result |= remove(o);
187         }
188         return result;
189     }
190 
191     @Override
192     public boolean retainAll(final Collection<?> c)
193     {
194         throw new RuntimeException("Not (yet) implemented");
195     }
196 
197     @Override
198     public void clear()
199     {
200         this.tree.clear();
201     }
202 
203     /**
204      * Increment the number of sub trees created.
205      */
206     void incrementSubTreeCount()
207     {
208         this.totalSubTrees++;
209     }
210 
211     /**
212      * Return the total number of sub trees.
213      * @return int; the total number of sub trees
214      */
215     public int getSubTreeCount()
216     {
217         return this.totalSubTrees;
218     }
219 
220     @Override
221     public String toString()
222     {
223         return "QuadTree [maximumLoad=" + this.maximumLoad + ", minimumSize=" + this.minimumSize + ", tree=" + this.tree + "]";
224     }
225 
226     /**
227      * Make a textual description of this quad tree drilling down to the prescribed depth.
228      * @param expandDepth int; maximum depth to descend
229      * @return String; textual description of this quad tree
230      */
231     public String toString(final int expandDepth)
232     {
233         return "QuadTree [maximumLoad=" + this.maximumLoad + ", minimumSize=" + this.minimumSize + ", tree="
234                 + this.tree.toString(expandDepth) + "]";
235     }
236 
237     /**
238      * Dump a quad tree.
239      * @param indent String; prefix for each output line
240      * @return String; textual description of this quad tree.
241      */
242     public String dump(final String indent)
243     {
244         return this.tree.dump(indent);
245     }
246 
247     /**
248      * Sub tree of a quad tree.
249      * @param <T> Type of object stored in this quad tree
250      */
251     @SuppressWarnings("hiding")
252     class SubTree<T extends Envelope> implements Serializable
253     {
254         /** ... */
255         private static final long serialVersionUID = 20200904L;
256 
257         /** Root of the quad tree. */
258         private final QuadTree<T> root;
259 
260         /** Bounding box of this quad tree. */
261         private final Rectangle boundingBox;
262 
263         /** Current number of objects in this quad tree. Includes all children, counting each object exactly once. */
264         private int size = 0;
265 
266         /** If the four children have been allocated, this array will be non-null and contain the four children. */
267         private SubTree<T>[] children = null;
268 
269         /** Elements stored at this node. */
270         private Set<RectangleAndPayload<T>> elements = null;
271 
272         /**
273          * Construct a new sub tree.
274          * @param root QuadTree&lt;T&gt;; the root
275          * @param boundingBox Rectangle; the bounding box of the new sub tree
276          */
277         SubTree(final QuadTree<T> root, final Rectangle boundingBox)
278         {
279             this.root = root;
280             this.boundingBox = boundingBox;
281             root.incrementSubTreeCount();
282         }
283 
284         /**
285          * Retrieve the bounding box of this sub tree.
286          * @return Rectangle; the bounding box of this sub tree
287          */
288         public final Rectangle getBoundingBox()
289         {
290             return this.boundingBox;
291         }
292 
293         /**
294          * Return the number of objects stored in and under this SubTree.
295          * @return int; the number of objects stored in and under this SubTree
296          */
297         public int size()
298         {
299             return this.size;
300         }
301 
302         /**
303          * Add a RectangleAndPayload to this SubTree.
304          * @param e RectangleAndPayload&lt;T&gt;; the object to add
305          * @return boolean; true if this SubTree was changed (object was added); false if this SubTree did not change
306          */
307         public boolean add(final RectangleAndPayload<T> e)
308         {
309             if (contains(e))
310             {
311                 return false;
312             }
313             if (this.elements == null)
314             {
315                 this.elements = new LinkedHashSet<>();
316             }
317             this.elements.add(e);
318             this.size++;
319             reBalance();
320             return true;
321         }
322 
323         /**
324          * Remove a RectangleAndPayload from this SubTree.
325          * @param o RectangleAndPayload&lt;T&gt;; the object to remove
326          * @return boolean; true if this SubTree was changed (object was removed); false if this SubTree did not change
327          */
328         public boolean remove(final RectangleAndPayload<T> o)
329         {
330             if (this.elements.remove(o))
331             {
332                 this.size--;
333                 return true; // The object cannot be also present in any of the sub trees
334             }
335             // Try all of the sub trees
336             boolean result = false;
337             if (this.children != null)
338             {
339                 Rectangle rectangle = o.getRectangle();
340                 for (SubTree<T> child : this.children)
341                 {
342                     if (!child.boundingBox.intersects(rectangle))
343                     {
344                         continue; // This is the time saver
345                     }
346                     if (child.remove(o))
347                     {
348                         result = true;
349                     }
350                 }
351             }
352             if (result)
353             {
354                 this.size--;
355             }
356             return result;
357         }
358 
359         /**
360          * Delete all objects stored in this SubTree.
361          */
362         public void clear()
363         {
364             this.elements.clear();
365             this.children = null;
366             this.size = 0;
367         }
368 
369         /**
370          * Determine if this SubTree contains a specific object.
371          * @param o RectangleAndPayload&lt;T&gt;; the object to search
372          * @return boolean; true if this SubTree contains the object
373          */
374         public boolean contains(final RectangleAndPayload<T> o)
375         {
376             if (this.elements == null)
377             {
378                 return false;
379             }
380             return recursiveContains(o);
381         }
382 
383         /**
384          * Recursively search for a particular object.
385          * @param o RectangleAndPayload&lt;T&gt;; the object to search for
386          * @return boolean; true if this quad tree contains the object; false if this quad tree does not contain the object
387          */
388         boolean recursiveContains(final RectangleAndPayload<T> o)
389         {
390             if ((!this.boundingBox.intersects(o.getRectangle())) || (this.elements == null))
391             {
392                 return false; // This is the time saver
393             }
394             for (RectangleAndPayload<T> element : this.elements)
395             {
396                 if (element.equals(o))
397                 {
398                     return true;
399                 }
400             }
401             if (this.children == null)
402             {
403                 return false;
404             }
405             for (SubTree<T> child : this.children)
406             {
407                 if (child.recursiveContains(o))
408                 {
409                     return true;
410                 }
411             }
412             return false;
413         }
414 
415         /**
416          * Recursively collect all elements that intersect the given rectangle.
417          * @param rectangle Rectangle; the rectangle
418          * @return Set&lt;RectangleAndPayload&lt;T&gt;&gt;; all stored elements that intersect the given rectangle
419          */
420         public Set<RectangleAndPayload<T>> recursiveCollect(final Rectangle rectangle)
421         {
422             Set<RectangleAndPayload<T>> result = new LinkedHashSet<>();
423             if (!this.boundingBox.intersects(rectangle))
424             {
425                 return result; // This is the time saver
426             }
427             if (this.elements != null)
428             {
429                 for (RectangleAndPayload<T> element : this.elements)
430                 {
431                     if (element.getRectangle().intersects(rectangle))
432                     {
433                         result.add(element);
434                     }
435                 }
436             }
437             if (this.children != null)
438             {
439                 for (SubTree<T> child : this.children)
440                 {
441                     result.addAll(child.recursiveCollect(rectangle));
442                 }
443             }
444             return result;
445         }
446 
447         /**
448          * Optimize the distribution of elements at this node and at sub-nodes.
449          */
450         @SuppressWarnings("unchecked")
451         private void reBalance()
452         {
453             if (this.elements.size() < this.root.getMaxLoad() || this.boundingBox.getWidth() < this.root.getMinimumSize()
454                     || this.boundingBox.getHeight() < this.root.getMinimumSize())
455             {
456                 return;
457             }
458             // Count the number of elements that could be moved down to sub-trees
459             double cX = (this.boundingBox.getLeft() + this.boundingBox.getRight()) / 2;
460             double cY = (this.boundingBox.getBottom() + this.boundingBox.getTop()) / 2;
461             int canMove = 0;
462             /*-
463             for (T e : this.elements)
464             {
465                 // This criterion is not good
466                 if (!e.getBoundingRectangle().contains(this.boundingBox))
467                 {
468                     canMove++;
469                 }
470             }
471             */
472             canMove = this.elements.size();
473             if (canMove == 0 || canMove < this.root.getMaxLoad() /* / 2 */ && this.children == null)
474             {
475                 // System.out.println("reBalance: not moving " + canMove + " of " + this.elements.size());
476                 return;
477             }
478             // System.out.println("At start of reBalance of " + this.toString(1));
479             if (this.children == null)
480             {
481                 this.children = new SubTree[] {
482                         new SubTree<T>(this.root,
483                                 new Rectangle(this.boundingBox.getLeft(), this.boundingBox.getBottom(), cX, cY)),
484                         new SubTree<T>(this.root,
485                                 new Rectangle(cX, this.boundingBox.getBottom(), this.boundingBox.getRight(), cY)),
486                         new SubTree<T>(this.root, new Rectangle(this.boundingBox.getLeft(), cY, cX, this.boundingBox.getTop())),
487                         new SubTree<T>(this.root,
488                                 new Rectangle(cX, cY, this.boundingBox.getRight(), this.boundingBox.getTop()))};
489             }
490             Iterator<RectangleAndPayload<T>> iterator = this.elements.iterator();
491             while (iterator.hasNext())
492             {
493                 RectangleAndPayload<T> e = iterator.next();
494                 if (e.getRectangle().contains(this.boundingBox))
495                 {
496                     continue;
497                 }
498                 boolean added = false;
499                 for (SubTree<T> child : this.children)
500                 {
501                     if (e.getRectangle().intersects(child.boundingBox))
502                     {
503                         added |= child.add(e);
504                     }
505                 }
506                 if (added)
507                 {
508                     iterator.remove();
509                 }
510                 else
511                 {
512                     System.out.println("ERROR: Could not add " + e + " to any of the children");
513                 }
514             }
515             // System.out.println("At end of reBalanceof " + this.toString(1));
516         }
517 
518         @Override
519         public String toString()
520         {
521             return "SubTree [boundingBox=" + this.boundingBox + ", size=" + this.size + ", children=" + this.children
522                     + (this.elements == null ? "elements=null" : ", elements.size=" + this.elements.size()) + "]";
523         }
524 
525         /**
526          * Return a textual representation of this quad tree up to the specified depth.
527          * @param expandDepth int; the maximum depth to expand
528          * @return String; textual representation of this quad tree
529          */
530         public String toString(final int expandDepth)
531         {
532             if (expandDepth > 0)
533             {
534                 return "SubTree [boundingBox=" + this.boundingBox + ", size=" + this.size + ", children="
535                         + (this.children != null ? "[SW:" + this.children[0].toString(expandDepth - 1) + ", SE:"
536                                 + this.children[1].toString(expandDepth - 1) + ", NW:"
537                                 + this.children[2].toString(expandDepth - 1) + ", NE:"
538                                 + this.children[3].toString(expandDepth - 1) + "]" : "null")
539                         + ", elements.size=" + this.elements.size() + "]";
540             }
541             else
542             {
543                 return toString();
544             }
545         }
546 
547         /**
548          * Dump a quad tree.
549          * @param indent String; prefix for each output line
550          * @return String; textual description of this quad tree.
551          */
552         public String dump(final String indent)
553         {
554             StringBuilder result = new StringBuilder();
555             result.append(indent);
556             result.append("SubTree [size=");
557             result.append(this.size);
558             result.append("] ");
559             result.append(this.boundingBox);
560             result.append("\n");
561             String subIndent = indent + "    ";
562             Iterator<RectangleAndPayload<T>> iterator = this.elements.iterator();
563             for (int i = 0; i < this.elements.size(); i++)
564             {
565                 result.append(subIndent);
566                 result.append(i);
567                 result.append(" ");
568                 result.append(iterator.next());
569                 result.append("\n");
570             }
571             if (this.children != null)
572             {
573                 result.append(subIndent);
574                 result.append("SW");
575                 result.append("\n");
576                 result.append(this.children[0].dump(subIndent));
577                 result.append(subIndent);
578                 result.append("SE");
579                 result.append("\n");
580                 result.append(this.children[1].dump(subIndent));
581                 result.append(subIndent);
582                 result.append("NW");
583                 result.append("\n");
584                 result.append(this.children[2].dump(subIndent));
585                 result.append(subIndent);
586                 result.append("NE");
587                 result.append("\n");
588                 result.append(this.children[3].dump(subIndent));
589             }
590             return result.toString();
591         }
592 
593     }
594 
595 }
596 
597 /**
598  * Container for a Rectangle and a payload.
599  * @param <T> Object; the payload
600  */
601 class RectangleAndPayload<T extends Object> implements Serializable
602 {
603     /** ... */
604     private static final long serialVersionUID = 20200904L;
605 
606     /** The bounding rectangle. */
607     private final Rectangle rectangle;
608 
609     /** The payload. */
610     private final T payload;
611 
612     /**
613      * Construct a new RectangleAndPayload object.
614      * @param rectangle Rectangle; the bounding rectangle of the payload
615      * @param payload T; the payload
616      */
617     RectangleAndPayload(final Rectangle rectangle, final T payload)
618     {
619         this.rectangle = rectangle;
620         this.payload = payload;
621     }
622 
623     /**
624      * Retrieve the bounding rectangle.
625      * @return Rectangle; the bounding rectangle
626      */
627     public Rectangle getRectangle()
628     {
629         return this.rectangle;
630     }
631 
632     /**
633      * Retrieve the payload.
634      * @return T; the payload
635      */
636     public T getPayload()
637     {
638         return this.payload;
639     }
640 
641     @Override
642     public String toString()
643     {
644         return "RectangleAndPayload [rectangle=" + this.rectangle + ", payload=" + this.payload + "]";
645     }
646 
647     @Override
648     public int hashCode()
649     {
650         final int prime = 31;
651         int result = 1;
652         result = prime * result + ((this.payload == null) ? 0 : this.payload.hashCode());
653         result = prime * result + ((this.rectangle == null) ? 0 : this.rectangle.hashCode());
654         return result;
655     }
656 
657     @Override
658     public boolean equals(final Object obj)
659     {
660         if (this == obj)
661         {
662             return true;
663         }
664         if (obj == null)
665         {
666             return false;
667         }
668         if (getClass() != obj.getClass())
669         {
670             return false;
671         }
672         @SuppressWarnings("rawtypes")
673         RectangleAndPayload other = (RectangleAndPayload) obj;
674         if (this.payload == null)
675         {
676             if (other.payload != null)
677             {
678                 return false;
679             }
680         }
681         else if (!this.payload.equals(other.payload))
682         {
683             return false;
684         }
685         if (this.rectangle == null)
686         {
687             if (other.rectangle != null)
688             {
689                 return false;
690             }
691         }
692         else if (!this.rectangle.equals(other.rectangle))
693         {
694             return false;
695         }
696         return true;
697     }
698 
699 }