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