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