View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.util.Collection;
4   import java.util.Iterator;
5   import java.util.LinkedHashSet;
6   import java.util.Set;
7   
8   import org.djutils.draw.bounds.Bounds2d;
9   import org.djutils.draw.line.Polygon2d;
10  import org.djutils.draw.point.Point2d;
11  import org.djutils.exceptions.Throw;
12  import org.opentrafficsim.base.logger.Logger;
13  
14  /**
15   * Set of Polygon2d objects and provides methods for fast selection of those objects that intersect a Polygon2d. <br>
16   * An Ots2dSet internally stores the Polygon2ds in a quad tree. At time of construction the minimum cell size is defined. Node
17   * expansion is never performed on nodes that are smaller than this limit. <br>
18   * Each node (even the non-leaf nodes) store a set of Polygon2d. Non-leaf nodes locally store those shapes that completely cover
19   * the rectangular area of the node. Such shapes are <b>not</b> also stored in leaf nodes below that node. Polygon2ds that
20   * partially cover a non-leaf node are stored in each of the leaf nodes below that node that those Polygon2ds (partially) cover.
21   * Leaf nodes that cannot be expanded (because they are too small) also store all Polygon2ds that partially cover the area of
22   * the node. <br>
23   * If removal of a Polygon2d objects results in a leaf becoming empty, that leaf is removed from its parent (which may then
24   * itself become empty and removed in turn).
25   * <p>
26   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
27   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
28   * </p>
29   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
30   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
31   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
32   */
33  public class Ots2dSet implements Set<Polygon2d>
34  {
35      /** Set of all shapes used for iterators, etc. */
36      private final Set<Polygon2d> allShapes = new LinkedHashSet<Polygon2d>();
37  
38      /** How fine will this quad tree divide. This one is copied to each sub-node which is somewhat inefficient. */
39      private final double minimumCellSize;
40  
41      /** Spatial storage for the Polygon2ds. */
42      private QuadTreeNode quadTree;
43  
44      /**
45       * Construct an empty Ots2dSet for a rectangular region. Objects that do not intersect this region will never be stored in
46       * this Ots2dSet. (Trying to add such a Polygon2d is <b>not</b> an error; the <code>add</code> method will return false,
47       * indicating that the set has not been modified.)
48       * @param boundingBox the region
49       * @param minimumCellSize resolution of the underlying quad tree
50       * @throws IllegalArgumentException when the bounding box covers no surface or the minimum cell size is not above zero
51       */
52      public Ots2dSet(final Bounds2d boundingBox, final double minimumCellSize) throws IllegalArgumentException
53      {
54          Throw.when(null == boundingBox, NullPointerException.class, "The boundingBox may not be null");
55          Throw.when(boundingBox.getDeltaX() <= 0 || boundingBox.getDeltaY() <= 0, IllegalArgumentException.class,
56                  "The boundingBox must have nonzero surface (got %s", boundingBox);
57          Throw.when(minimumCellSize <= 0, IllegalArgumentException.class, "The minimumCellSize must be > 0 (got %f)",
58                  minimumCellSize);
59          this.quadTree = new QuadTreeNode(boundingBox);
60          this.minimumCellSize = minimumCellSize;
61      }
62  
63      @Override
64      public final int size()
65      {
66          return this.allShapes.size();
67      }
68  
69      @Override
70      public final boolean isEmpty()
71      {
72          return this.allShapes.isEmpty();
73      }
74  
75      @Override
76      public final boolean contains(final Object o)
77      {
78          return this.allShapes.contains(o);
79      }
80  
81      @Override
82      public final Iterator<Polygon2d> iterator()
83      {
84          return new QuadTreeIterator();
85      }
86  
87      @Override
88      public final Object[] toArray()
89      {
90          return this.allShapes.toArray();
91      }
92  
93      @Override
94      public final <T> T[] toArray(final T[] a)
95      {
96          return this.allShapes.toArray(a);
97      }
98  
99      @Override
100     public final boolean add(final Polygon2d e)
101     {
102         if (!this.quadTree.intersects(e))
103         {
104             return false;
105         }
106         if (this.allShapes.contains(e))
107         {
108             return false;
109         }
110         if (!this.quadTree.add(e))
111         {
112             Logger.ots().error("add: ERROR object could not be added to the quad tree");
113         }
114         return this.allShapes.add(e);
115     }
116 
117     @Override
118     public final boolean remove(final Object o)
119     {
120         if (!this.allShapes.remove(o))
121         {
122             return false;
123         }
124         if (!this.quadTree.remove((Polygon2d) o))
125         {
126             Logger.ots().error("remove: ERROR object could not be removed from the quad tree");
127         }
128         return true;
129     }
130 
131     @Override
132     public final boolean containsAll(final Collection<?> c)
133     {
134         for (Object o : c)
135         {
136             if (!contains(o))
137             {
138                 return false;
139             }
140         }
141         return true;
142     }
143 
144     @Override
145     public final boolean addAll(final Collection<? extends Polygon2d> c)
146     {
147         boolean result = false;
148         for (Polygon2d s : c)
149         {
150             if (add(s))
151             {
152                 result = true;
153             }
154         }
155         return result;
156     }
157 
158     @Override
159     public final boolean retainAll(final Collection<?> c)
160     {
161         boolean result = false;
162         for (Iterator<Polygon2d> it = iterator(); it.hasNext();)
163         {
164             Polygon2d shape = it.next();
165             if (!c.contains(shape))
166             {
167                 it.remove();
168                 result = true;
169             }
170         }
171         return result;
172     }
173 
174     @Override
175     public final boolean removeAll(final Collection<?> c)
176     {
177         boolean result = false;
178         for (Iterator<Polygon2d> it = iterator(); it.hasNext();)
179         {
180             Polygon2d shape = it.next();
181             if (c.contains(shape))
182             {
183                 it.remove();
184                 result = true;
185             }
186         }
187         return result;
188     }
189 
190     @Override
191     public final void clear()
192     {
193         this.quadTree.clear();
194         this.allShapes.clear();
195     }
196 
197     /**
198      * Return the set of all shapes in this Ots2dSet that intersect the given rectangle.
199      * @param rectangle the rectangle
200      * @return the shapes that intersect the rectangle
201      */
202     public final Set<Polygon2d> intersectingShapes(final Bounds2d rectangle)
203     {
204         return this.quadTree.intersectingShapes(rectangle);
205     }
206 
207     /**
208      * Recursively print this Ots2dSet.
209      * @param recursionDepth maximum depth to recurse
210      * @return String
211      */
212     final String toString(final int recursionDepth)
213     {
214         return "Ots2dSet [contains " + size() + (1 == this.allShapes.size() ? "shape" : "shapes") + ", minimumCellSize="
215                 + this.minimumCellSize + ", quadTree=" + this.quadTree.toString(recursionDepth) + "]";
216     }
217 
218     @Override
219     public final String toString()
220     {
221         return toString(0);
222     }
223 
224     /**
225      * Return all Polygon2ds in this Ots2dSet that intersect a given Polygon2d.
226      * @param shape the given Polygon2d
227      * @return all Polygon2ds in this Ots2dSet that intersect <code>shape</code>
228      */
229     public final Set<Polygon2d> intersectingShapes(final Polygon2d shape)
230     {
231         Bounds2d bounds = shape.getAbsoluteBounds();
232         Set<Polygon2d> result =
233                 intersectingShapes(new Bounds2d(bounds.getMinX(), bounds.getMinY(), bounds.getDeltaX(), bounds.getDeltaY()));
234         for (Iterator<Polygon2d> it = result.iterator(); it.hasNext();)
235         {
236             if (!it.next().intersects(shape))
237             {
238                 it.remove();
239             }
240         }
241         return result;
242     }
243 
244     /**
245      * Return an ASCII art rendering of this Ots2dSet.
246      * @param recursionDepth maximum recursion depth
247      * @return a somewhat human readable rendering of this Ots2dSet
248      */
249     public final String toStringGraphic(final int recursionDepth)
250     {
251         return this.quadTree.toStringGraphic(recursionDepth);
252     }
253 
254     /**
255      * Iterator for quad tree. Shall iterate over the local set of shapes and the (up to four) non-null leave nodes.
256      */
257     class QuadTreeIterator implements Iterator<Polygon2d>
258     {
259         /** Underlying iterator that traverses the allShapes Set. */
260         private final Iterator<Polygon2d> theIterator = Ots2dSet.this.allShapes.iterator();
261 
262         /** Remember the last returned result so we can remove it when requested. */
263         private Polygon2d lastResult = null;
264 
265         /**
266          * Constructor.
267          */
268         QuadTreeIterator()
269         {
270             //
271         }
272 
273         @Override
274         public final boolean hasNext()
275         {
276             return this.theIterator.hasNext();
277         }
278 
279         @Override
280         public final Polygon2d next()
281         {
282             this.lastResult = this.theIterator.next();
283             return this.lastResult;
284         }
285 
286         @Override
287         public final void remove()
288         {
289             this.theIterator.remove();
290             if (!Ots2dSet.this.quadTree.remove(this.lastResult))
291             {
292                 Logger.ots().error("iterator.remove: ERROR: could not remove {} from the quad tree",
293                         this.lastResult);
294             }
295         }
296 
297         @Override
298         public String toString()
299         {
300             return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
301         }
302 
303     }
304 
305     /**
306      * Spatial-aware storage for a set of Polygon2d objects.
307      */
308     class QuadTreeNode
309     {
310         /** The Polygon2ds stored at this node. */
311         private Set<Polygon2d> shapes = new LinkedHashSet<Polygon2d>();
312 
313         /** The bounding box of this QuadTreeNode. */
314         private final Bounds2d boundingBox;
315 
316         /** The bounding box of this QuadTreeNode as a Polygon2d. */
317         private final Polygon2d boundingShape;
318 
319         /**
320          * The four leaves of this node in the quad tree. An empty sub tree may be represented by null. If this field is
321          * initialized to null; this node may not expand by adding sub-nodes.
322          */
323         private final QuadTreeNode[] leaves;
324 
325         /**
326          * Construct a new QuadTreeNode.
327          * @param boundingBox the bounding box of the area of the new QuadTreeNode
328          */
329         QuadTreeNode(final Bounds2d boundingBox)
330         {
331             this.boundingBox = boundingBox;
332             this.boundingShape = rectangleShape(boundingBox);
333             this.leaves = boundingBox.getDeltaY() > Ots2dSet.this.minimumCellSize
334                     || boundingBox.getDeltaX() > Ots2dSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
335         }
336 
337         /**
338          * Return a Set containing all Polygon2ds in this QuadTreeNode that intersect a rectangular area.
339          * @param rectangle the area
340          * @return the set
341          */
342         public Set<Polygon2d> intersectingShapes(final Bounds2d rectangle)
343         {
344             Set<Polygon2d> result = new LinkedHashSet<Polygon2d>();
345             if (!this.boundingBox.intersects(rectangle))
346             {
347                 return result;
348             }
349             if (null == this.leaves)
350             {
351                 return result;
352             }
353             for (QuadTreeNode leaf : this.leaves)
354             {
355                 if (null != leaf && leaf.intersects(rectangle))
356                 {
357                     result.addAll(leaf.intersectingShapes(rectangle));
358                 }
359             }
360             for (Polygon2d shape : this.shapes)
361             {
362                 Polygon2d rectangleShape = rectangleShape(rectangle);
363                 if (rectangleShape.intersects(shape))
364                 {
365                     result.add(shape);
366                 }
367             }
368             return result;
369         }
370 
371         /**
372          * Test if this QuadTreeNode intersects a rectangular area.
373          * @param rectangle the rectangular area
374          * @return true if the rectangular area intersects this QuadTreeNode; false otherwise
375          */
376         private boolean intersects(final Bounds2d rectangle)
377         {
378             return this.boundingBox.intersects(rectangle);
379         }
380 
381         /**
382          * Remove all Polygon2ds from this QuadTreeNode and cut off all leaves.
383          */
384         public void clear()
385         {
386             this.shapes.clear();
387             for (int index = 0; index < this.leaves.length; index++)
388             {
389                 this.leaves[index] = null;
390             }
391         }
392 
393         /**
394          * Remove a Polygon2d from this QuadTreeNode.
395          * @param shape the shape that must be removed.
396          * @return true if this node (or a sub-node) was altered; false otherwise
397          */
398         public boolean remove(final Polygon2d shape)
399         {
400             if (!this.boundingShape.intersects(shape))
401             {
402                 return false;
403             }
404             for (Polygon2d s : this.shapes)
405             {
406                 if (shape.equals(s))
407                 {
408                     this.shapes.remove(shape);
409                     return true;
410                 }
411             }
412             boolean result = false;
413             for (int index = 0; index < this.leaves.length; index++)
414             {
415                 QuadTreeNode qtn = this.leaves[index];
416                 if (null != qtn)
417                 {
418                     if (qtn.remove(shape))
419                     {
420                         result = true;
421                         if (qtn.isEmpty())
422                         {
423                             this.leaves[index] = null; // Cut off empty leaf node
424                         }
425                     }
426                 }
427             }
428             return result;
429         }
430 
431         /**
432          * Check if this QuadTreeNode is empty.
433          * @return true if this QuadTreeNode is empty
434          */
435         private boolean isEmpty()
436         {
437             if (!this.shapes.isEmpty())
438             {
439                 return false;
440             }
441             if (null == this.leaves)
442             {
443                 return true;
444             }
445             for (QuadTreeNode qtn : this.leaves)
446             {
447                 if (null != qtn)
448                 {
449                     return false;
450                 }
451             }
452             return true;
453         }
454 
455         /**
456          * Test if the area of this QuadTree intersects a Polygon2d.
457          * @param shape the shape
458          * @return true if the area of this QuadTree intersects the shape; false otherwise
459          */
460         public boolean intersects(final Polygon2d shape)
461         {
462             return this.boundingShape.intersects(shape);
463         }
464 
465         /**
466          * Construct a Polygon2d from a Rectangle2D.
467          * @param rectangle the rectangle
468          * @return a new Polygon2d
469          */
470         private Polygon2d rectangleShape(final Bounds2d rectangle)
471         {
472             double left = rectangle.getMinX();
473             double bottom = rectangle.getMinY();
474             double right = rectangle.getMaxX();
475             double top = rectangle.getMaxY();
476             return new Polygon2d(new Point2d(left, bottom), new Point2d(right, bottom), new Point2d(right, top),
477                     new Point2d(left, top));
478         }
479 
480         /**
481          * Add a Polygon2d to this QuadTreeNode.
482          * @param shape the shape
483          * @return true if this QuadTreeNode changed as a result of this operation
484          */
485         public final boolean add(final Polygon2d shape)
486         {
487             if (!this.boundingShape.intersects(shape))
488             {
489                 return false;
490             }
491             if ((null == this.leaves) || shape.contains(this.boundingBox))
492             {
493                 // shape belongs in the set of shapes of this node.
494                 return this.shapes.add(shape);
495             }
496             // This node may have leaves and shape does not entirely contain this node. Add shape to all applicable leaves.
497             boolean result = false;
498             for (int index = 0; index < this.leaves.length; index++)
499             {
500                 if (null == this.leaves[index])
501                 {
502                     double subWidth = this.boundingBox.getDeltaX() / 2;
503                     double subHeight = this.boundingBox.getDeltaY() / 2;
504                     if (0 == subWidth)
505                     {
506                         // loss of precision; degenerate into a binary tree
507                         subWidth = this.boundingBox.getDeltaX();
508                     }
509                     if (0 == subHeight)
510                     {
511                         // loss of precision; degenerate into a binary tree
512                         subHeight = this.boundingBox.getDeltaY();
513                     }
514                     double left = this.boundingBox.getMinX();
515                     if (0 != index / 2)
516                     {
517                         left += subWidth;
518                     }
519                     double bottom = this.boundingBox.getMinY();
520                     if (0 != index % 2)
521                     {
522                         bottom += subHeight;
523                     }
524                     Bounds2d subBox = new Bounds2d(left, left + subWidth, bottom, bottom + subHeight);
525                     if (rectangleShape(subBox).intersects(shape))
526                     {
527                         // Expand this node by adding a sub node.
528                         this.leaves[index] = new QuadTreeNode(subBox);
529                         if (this.leaves[index].add(shape))
530                         {
531                             result = true;
532                         }
533                         else
534                         {
535                             throw new Error("Cannot happen: new QuadTreeNode refused to add shape that intersects it");
536                         }
537                     }
538                 }
539                 else
540                 {
541                     // Leaf node already exists. Let the leaf determine if shape should be stored (somewhere) in it.
542                     if (this.leaves[index].add(shape))
543                     {
544                         result = true;
545                     }
546                 }
547             }
548             return result;
549         }
550 
551         /**
552          * Helper function for toString.
553          * @param recursionDepth maximum number of levels to print recursively
554          * @param index index in leaves
555          * @return String
556          */
557         private String printLeaf(final int recursionDepth, final int index)
558         {
559             QuadTreeNode leaf = this.leaves[index];
560             if (null == leaf)
561             {
562                 return "null";
563             }
564             if (recursionDepth > 0)
565             {
566                 return leaf.toString(recursionDepth - 1);
567             }
568             int leafSize = leaf.shapes.size();
569             return leafSize + " shape" + (1 == leafSize ? "" : "s");
570         }
571 
572         /**
573          * Recursively print this QuadTreeNode.
574          * @param recursionDepth maximum depth to recurse
575          * @return String
576          */
577         final String toString(final int recursionDepth)
578         {
579             return "QuadTreeNode [" + this.shapes.size() + ", bounds=[LB: " + this.boundingBox.getMinX() + ","
580                     + this.boundingBox.getMinY() + ", RT: " + this.boundingBox.getMaxX() + "," + this.boundingBox.getMaxY()
581                     + "], " + subNodes(recursionDepth) + ", local " + this.shapes.size()
582                     + (1 == this.shapes.size() ? " shape" : " shapes") + "]";
583         }
584 
585         /**
586          * Print the leaves of this QuadTreeNode.
587          * @param recursionDepth maximum depth to recurse
588          * @return String
589          */
590         private String subNodes(final int recursionDepth)
591         {
592             if (null == this.leaves)
593             {
594                 return "cannot have leaves";
595             }
596             return "leaves=[LB: " + printLeaf(recursionDepth, 0) + ", RB: " + printLeaf(recursionDepth, 1) + ", LT: "
597                     + printLeaf(recursionDepth, 2) + ", RT: " + printLeaf(recursionDepth, 3) + "]";
598         }
599 
600         @Override
601         public final String toString()
602         {
603             return toString(0);
604         }
605 
606         /**
607          * Return concatenation of a number of copies of a string.
608          * @param count number of copies to concatenate
609          * @param string the string to repeat
610          * @return String
611          */
612         private String repeat(final int count, final String string)
613         {
614             StringBuilder result = new StringBuilder();
615             for (int i = 0; i < count; i++)
616             {
617                 result.append(string);
618             }
619             return result.toString();
620         }
621 
622         /** Graphic to draw a vertical line. */
623         private static final String VLINE = "|";
624 
625         /** Graphic to draw a horizontal line. */
626         private static final String HLINE = "-";
627 
628         /** Graphic to draw a space. */
629         private static final String SPACE = " ";
630 
631         /** Number of digits to print. */
632         private static final int NUMBERSIZE = 6;
633 
634         /**
635          * Similar to toStringGraphic, but with QuadTreeNode argument which can be null. <br>
636          * This code is <b>not</b> optimized for performance; the repeated use of String.split is probably expensive.
637          * @param qtn the QuadTreeNode to render. Can be null.
638          * @param recursionDepth levels to recurse
639          * @return String
640          */
641         private String subStringGraphic(final QuadTreeNode qtn, final int recursionDepth)
642         {
643             StringBuffer result = new StringBuffer();
644             if (0 == recursionDepth)
645             {
646                 if (null == qtn)
647                 {
648                     result.append(repeat(NUMBERSIZE, SPACE));
649                 }
650                 else
651                 {
652                     String numberBuf = String.format("%d", size());
653                     int spare = NUMBERSIZE - numberBuf.length();
654                     int filled = 0;
655                     while (filled < spare / 2)
656                     {
657                         result.append(SPACE);
658                         filled++;
659                     }
660                     result.append(numberBuf);
661                     while (filled < spare)
662                     {
663                         result.append(SPACE);
664                         filled++;
665                     }
666                     result.append("\n");
667                     return result.toString();
668                 }
669             }
670             else
671             {
672                 String[] left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1)
673                         .split("\\n");
674                 String[] right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1)
675                         .split("\\n");
676                 String horizontalLine = null;
677                 for (int i = 0; i < left.length; i++)
678                 {
679                     if (0 == i)
680                     {
681                         StringBuilder line = new StringBuilder();
682                         int width = left[0].length() + 1 + right[0].length();
683                         if (null == qtn)
684                         {
685                             line.append(repeat(width, SPACE));
686                         }
687                         else
688                         {
689                             String numberBuf = String.format("%d", qtn.shapes.size());
690                             int spare = width - numberBuf.length();
691                             line.append(repeat(spare / 2, HLINE));
692                             line.append(numberBuf);
693                             line.append(repeat(spare - spare / 2, HLINE));
694                         }
695                         horizontalLine = line.toString();
696                     }
697                     result.append(left[i]);
698                     result.append(null == qtn ? SPACE : VLINE);
699                     result.append(right[i]);
700                     result.append("\n");
701                 }
702                 result.append(horizontalLine);
703                 result.append("\n");
704                 left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1)
705                         .split("\\n");
706                 right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1)
707                         .split("\\n");
708                 for (int i = 0; i < left.length; i++)
709                 {
710                     result.append(left[i]);
711                     result.append(null == qtn ? SPACE : VLINE);
712                     result.append(right[i]);
713                     result.append("\n");
714                 }
715                 result.append("\n");
716             }
717             return result.toString();
718         }
719 
720         /**
721          * Return a String depicting this QuadTreeNode.
722          * @param recursionDepth levels to recurse
723          * @return String
724          */
725         public final String toStringGraphic(final int recursionDepth)
726         {
727             return subStringGraphic(this, recursionDepth);
728         }
729 
730     }
731 
732 }