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