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.HashSet;
7   import java.util.Iterator;
8   import java.util.Set;
9   
10  import org.djutils.exceptions.Throw;
11  import org.locationtech.jts.geom.Envelope;
12  
13  import nl.tudelft.simulation.dsol.logger.SimLogger;
14  
15  /**
16   * Set of OTSShape objects and provides methods for fast selection of those objects that intersect an OTSShape. <br>
17   * An OTS2DSet internally stores the OTSShapes in a quad tree. At time of construction the minimum cell size is defined. Node
18   * expansion is never performed on nodes that are smaller than this limit. <br>
19   * Each node (even the non-leaf nodes) store a set of OTSShape. Non-leaf nodes locally store those shapes that completely cover
20   * the rectangular area of the node. Such shapes are <b>not</b> also stored in leaf nodes below that node. OTSShapes that
21   * partially cover a non-leaf node are stored in each of the leaf nodes below that node that those OTSShapes (partially) cover.
22   * Leaf nodes that cannot be expanded (because they are too small) also store all OTSShapes that partially cover the area of the
23   * node. <br>
24   * If removal of an OTSShape objects results in a leaf becoming empty, that leaf is removed from its parent (which may then
25   * itself become empty and removed in turn).
26   * <p>
27   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
28   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
29   * <p>
30   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jun 20, 2016 <br>
31   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
32   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
33   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
34   */
35  public class OTS2DSet implements Set<OTSShape>, Serializable
36  {
37      /** */
38      private static final long serialVersionUID = 20170400L;
39  
40      /** Set of all shapes used for iterators, etc. */
41      private final Set<OTSShape> allShapes = new HashSet<OTSShape>();
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 OTSShapes. */
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 an OTSShape 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 Rectangle2D boundingBox, final double minimumCellSize) throws OTSGeometryException
58      {
59          Throw.when(null == boundingBox, NullPointerException.class, "The boundingBox may not be null");
60          Throw.when(boundingBox.getWidth() <= 0 || boundingBox.getHeight() <= 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<OTSShape> 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 OTSShape 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             SimLogger.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((OTSShape) o))
138         {
139             SimLogger.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 OTSShape> c)
161     {
162         boolean result = false;
163         for (OTSShape 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<OTSShape> it = iterator(); it.hasNext();)
179         {
180             OTSShape 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<OTSShape> it = iterator(); it.hasNext();)
196         {
197             OTSShape 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;OTSShape&gt;; the shapes that intersect the rectangle
219      */
220     public final Set<OTSShape> intersectingShapes(final Rectangle2D 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 OTSShapes in this OTS2DSet that intersect a given OTSShape.
245      * @param shape OTSShape; the given OTSShape
246      * @return Set&lt;OTSShape&gt;; all OTSShapes in this OTS2DSet that intersect <code>shape</code>
247      */
248     public final Set<OTSShape> intersectingShapes(final OTSShape shape)
249     {
250         Envelope envelope = shape.getEnvelope();
251         Set<OTSShape> result = intersectingShapes(
252                 new Rectangle2D.Double(envelope.getMinX(), envelope.getMinY(), envelope.getWidth(), envelope.getHeight()));
253         for (Iterator<OTSShape> 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<OTSShape>, 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<OTSShape> theIterator = OTS2DSet.this.allShapes.iterator();
284 
285         /** Remember the last returned result so we can remove it when requested. */
286         private OTSShape 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 OTSShape 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                 SimLogger.always().error("iterator.remove: ERROR: could not remove {} from the quad tree", this.lastResult);
312             }
313         }
314 
315         /** {@inheritDoc} */
316         @Override
317         public String toString()
318         {
319             return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
320         }
321 
322     }
323 
324     /**
325      * Spatial-aware storage for a set of OTSShape objects.
326      */
327     class QuadTreeNode implements Serializable
328     {
329         /** */
330         private static final long serialVersionUID = 20170400L;
331 
332         /** The OTSShapes stored at this node. */
333         private Set<OTSShape> shapes = new HashSet<OTSShape>();
334 
335         /** The bounding box of this QuadTreeNode. */
336         private final Rectangle2D boundingBox;
337 
338         /** The bounding box of this QuadTreeNode as an OTSShape. */
339         private final OTSShape boundingShape;
340 
341         /**
342          * The four leaves of this node in the quad tree. An empty sub tree may be represented by null. If this field is
343          * initialized to null; this node may not expand by adding sub-nodes.
344          */
345         private final QuadTreeNode[] leaves;
346 
347         /**
348          * Construct a new QuadTreeNode.
349          * @param boundingBox Rectangle2D; the bounding box of the area of the new QuadTreeNode
350          */
351         @SuppressWarnings("synthetic-access")
352         QuadTreeNode(final Rectangle2D boundingBox)
353         {
354             this.boundingBox = boundingBox;
355             this.boundingShape = rectangleShape(boundingBox);
356             this.leaves = boundingBox.getWidth() > OTS2DSet.this.minimumCellSize
357                     || boundingBox.getHeight() > OTS2DSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
358         }
359 
360         /**
361          * Return a Set containing all OTSShapes in this QuadTreeNode that intersect a rectangular area.
362          * @param rectangle Rectangle2D; the area
363          * @return Set&lt;OTSShape&gt;; the set
364          */
365         public Set<OTSShape> intersectingShapes(final Rectangle2D rectangle)
366         {
367             Set<OTSShape> result = new HashSet<OTSShape>();
368             if (!this.boundingBox.intersects(rectangle))
369             {
370                 return result;
371             }
372             if (null == this.leaves)
373             {
374                 return result;
375             }
376             for (QuadTreeNode leaf : this.leaves)
377             {
378                 if (null != leaf && leaf.intersects(rectangle))
379                 {
380                     result.addAll(leaf.intersectingShapes(rectangle));
381                 }
382             }
383             for (OTSShape shape : this.shapes)
384             {
385                 OTSShape rectangleShape = rectangleShape(rectangle);
386                 if (rectangleShape.intersects(shape))
387                 {
388                     result.add(shape);
389                 }
390             }
391             return result;
392         }
393 
394         /**
395          * Test if this QuadTreeNode intersects a rectangular area.
396          * @param rectangle Rectangle2D; the rectangular area
397          * @return boolean; true if the rectangular area intersects this QuadTreeNode; false otherwise
398          */
399         private boolean intersects(final Rectangle2D rectangle)
400         {
401             return this.boundingBox.intersects(rectangle);
402         }
403 
404         /**
405          * Remove all OTSShapes from this QuadTreeNode and cut off all leaves.
406          */
407         public void clear()
408         {
409             this.shapes.clear();
410             for (int index = 0; index < this.leaves.length; index++)
411             {
412                 this.leaves[index] = null;
413             }
414         }
415 
416         /**
417          * Remove an OTSShape from this QuadTreeNode.
418          * @param shape OTSShape; the shape that must be removed.
419          * @return boolean; true if this node (or a sub-node) was altered; false otherwise
420          */
421         public boolean remove(final OTSShape shape)
422         {
423             if (!this.boundingShape.intersects(shape))
424             {
425                 return false;
426             }
427             for (OTSShape s : this.shapes)
428             {
429                 if (shape.equals(s))
430                 {
431                     this.shapes.remove(shape);
432                     return true;
433                 }
434             }
435             boolean result = false;
436             for (int index = 0; index < this.leaves.length; index++)
437             {
438                 QuadTreeNode qtn = this.leaves[index];
439                 if (null != qtn)
440                 {
441                     if (qtn.remove(shape))
442                     {
443                         result = true;
444                         if (qtn.isEmpty())
445                         {
446                             this.leaves[index] = null; // Cut off empty leaf node
447                         }
448                     }
449                 }
450             }
451             return result;
452         }
453 
454         /**
455          * Check if this QuadTreeNode is empty.
456          * @return boolean; true if this QuadTreeNode is empty
457          */
458         private boolean isEmpty()
459         {
460             if (!this.shapes.isEmpty())
461             {
462                 return false;
463             }
464             if (null == this.leaves)
465             {
466                 return true;
467             }
468             for (QuadTreeNode qtn : this.leaves)
469             {
470                 if (null != qtn)
471                 {
472                     return false;
473                 }
474             }
475             return true;
476         }
477 
478         /**
479          * Test if the area of this QuadTree intersects an OTSShape.
480          * @param shape OTSShape; the shape
481          * @return boolean; true if the area of this QuadTree intersects the shape; false otherwise
482          */
483         public boolean intersects(final OTSShape shape)
484         {
485             return this.boundingShape.intersects(shape);
486         }
487 
488         /**
489          * Construct a OTSShape from a Rectangle2D.
490          * @param rectangle Rectangle2D; the rectangle
491          * @return OTSShape; a new OTSShape
492          */
493         private OTSShape rectangleShape(final Rectangle2D rectangle)
494         {
495             double left = rectangle.getMinX();
496             double bottom = rectangle.getMinY();
497             double right = rectangle.getMaxX();
498             double top = rectangle.getMaxY();
499             try
500             {
501                 return new OTSShape(new OTSPoint3D(left, bottom), new OTSPoint3D(right, bottom), new OTSPoint3D(right, top),
502                         new OTSPoint3D(left, top));
503             }
504             catch (OTSGeometryException exception)
505             {
506                 SimLogger.always().error(exception);
507                 return null;
508             }
509         }
510 
511         /**
512          * Add an OTSShape to this QuadTreeNode.
513          * @param shape OTSShape; the shape
514          * @return boolean; true if this QuadTreeNode changed as a result of this operation
515          */
516         public final boolean add(final OTSShape shape)
517         {
518             if (!this.boundingShape.intersects(shape))
519             {
520                 return false;
521             }
522             if ((null == this.leaves) || shape.contains(this.boundingBox))
523             {
524                 // shape belongs in the set of shapes of this node.
525                 return this.shapes.add(shape);
526             }
527             // This node may have leaves and shape does not entirely contain this node. Add shape to all applicable leaves.
528             boolean result = false;
529             for (int index = 0; index < this.leaves.length; index++)
530             {
531                 if (null == this.leaves[index])
532                 {
533                     double subWidth = this.boundingBox.getWidth() / 2;
534                     double subHeight = this.boundingBox.getHeight() / 2;
535                     if (0 == subWidth)
536                     {
537                         // loss of precision; degenerate into a binary tree
538                         subWidth = this.boundingBox.getWidth();
539                     }
540                     if (0 == subHeight)
541                     {
542                         // loss of precision; degenerate into a binary tree
543                         subHeight = this.boundingBox.getHeight();
544                     }
545                     double left = this.boundingBox.getMinX();
546                     if (0 != index / 2)
547                     {
548                         left += subWidth;
549                     }
550                     double bottom = this.boundingBox.getMinY();
551                     if (0 != index % 2)
552                     {
553                         bottom += subHeight;
554                     }
555                     Rectangle2D subBox = new Rectangle2D.Double(left, bottom, subWidth, subHeight);
556                     if (rectangleShape(subBox).intersects(shape))
557                     {
558                         // Expand this node by adding a sub node.
559                         this.leaves[index] = new QuadTreeNode(subBox);
560                         if (this.leaves[index].add(shape))
561                         {
562                             result = true;
563                         }
564                         else
565                         {
566                             throw new Error("Cannot happen: new QuadTreeNode refused to add shape that intersects it");
567                         }
568                     }
569                 }
570                 else
571                 {
572                     // Leaf node already exists. Let the leaf determine if shape should be stored (somewhere) in it.
573                     if (this.leaves[index].add(shape))
574                     {
575                         result = true;
576                     }
577                 }
578             }
579             return result;
580         }
581 
582         /**
583          * Helper function for toString.
584          * @param recursionDepth int; maximum number of levels to print recursively
585          * @param index int; index in leaves
586          * @return String
587          */
588         private String printLeaf(final int recursionDepth, final int index)
589         {
590             QuadTreeNode leaf = this.leaves[index];
591             if (null == leaf)
592             {
593                 return "null";
594             }
595             if (recursionDepth > 0)
596             {
597                 return leaf.toString(recursionDepth - 1);
598             }
599             int leafSize = leaf.shapes.size();
600             return leafSize + " shape" + (1 == leafSize ? "" : "s");
601         }
602 
603         /**
604          * Recursively print this QuadTreeNode.
605          * @param recursionDepth int; maximum depth to recurse
606          * @return String
607          */
608         final String toString(final int recursionDepth)
609         {
610             return "QuadTreeNode [" + this.shapes.size() + ", bounds=[LB: " + this.boundingBox.getMinX() + ","
611                     + this.boundingBox.getMinY() + ", RT: " + this.boundingBox.getMaxX() + "," + this.boundingBox.getMaxY()
612                     + "], " + subNodes(recursionDepth) + ", local " + this.shapes.size()
613                     + (1 == this.shapes.size() ? " shape" : " shapes") + "]";
614         }
615 
616         /**
617          * Print the leaves of this QuadTreeNode.
618          * @param recursionDepth int; maximum depth to recurse
619          * @return String
620          */
621         private String subNodes(final int recursionDepth)
622         {
623             if (null == this.leaves)
624             {
625                 return "cannot have leaves";
626             }
627             return "leaves=[LB: " + printLeaf(recursionDepth, 0) + ", RB: " + printLeaf(recursionDepth, 1) + ", LT: "
628                     + printLeaf(recursionDepth, 2) + ", RT: " + printLeaf(recursionDepth, 3) + "]";
629         }
630 
631         /** {@inheritDoc} */
632         @Override
633         public final String toString()
634         {
635             return toString(0);
636         }
637 
638         /**
639          * Return concatenation of a number of copies of a string.
640          * @param count int; number of copies to concatenate
641          * @param string String; the string to repeat
642          * @return String
643          */
644         private String repeat(final int count, final String string)
645         {
646             StringBuilder result = new StringBuilder();
647             for (int i = 0; i < count; i++)
648             {
649                 result.append(string);
650             }
651             return result.toString();
652         }
653 
654         /** Graphic to draw a vertical line. */
655         private static final String VLINE = "|";
656 
657         /** Graphic to draw a horizontal line. */
658         private static final String HLINE = "-";
659 
660         /** Graphic to draw a space. */
661         private static final String SPACE = " ";
662 
663         /** Number of digits to print. */
664         private static final int NUMBERSIZE = 6;
665 
666         /**
667          * Similar to toStringGraphic, but with QuadTreeNode argument which can be null. <br>
668          * This code is <b>not</b> optimized for performance; the repeated use of String.split is probably expensive.
669          * @param qtn QuadTreeNode; the QuadTreeNode to render. Can be null.
670          * @param recursionDepth int; levels to recurse
671          * @return String
672          */
673         private String subStringGraphic(final QuadTreeNode qtn, final int recursionDepth)
674         {
675             StringBuffer result = new StringBuffer();
676             if (0 == recursionDepth)
677             {
678                 if (null == qtn)
679                 {
680                     result.append(repeat(NUMBERSIZE, SPACE));
681                 }
682                 else
683                 {
684                     String numberBuf = String.format("%d", size());
685                     int spare = NUMBERSIZE - numberBuf.length();
686                     int filled = 0;
687                     while (filled < spare / 2)
688                     {
689                         result.append(SPACE);
690                         filled++;
691                     }
692                     result.append(numberBuf);
693                     while (filled < spare)
694                     {
695                         result.append(SPACE);
696                         filled++;
697                     }
698                     result.append("\n");
699                     return result.toString();
700                 }
701             }
702             else
703             {
704                 String[] left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1)
705                         .split("\\n");
706                 String[] right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1)
707                         .split("\\n");
708                 String horizontalLine = null;
709                 for (int i = 0; i < left.length; i++)
710                 {
711                     if (0 == i)
712                     {
713                         StringBuilder line = new StringBuilder();
714                         int width = left[0].length() + 1 + right[0].length();
715                         if (null == qtn)
716                         {
717                             line.append(repeat(width, SPACE));
718                         }
719                         else
720                         {
721                             String numberBuf = String.format("%d", qtn.shapes.size());
722                             int spare = width - numberBuf.length();
723                             line.append(repeat(spare / 2, HLINE));
724                             line.append(numberBuf);
725                             line.append(repeat(spare - spare / 2, HLINE));
726                         }
727                         horizontalLine = line.toString();
728                     }
729                     result.append(left[i]);
730                     result.append(null == qtn ? SPACE : VLINE);
731                     result.append(right[i]);
732                     result.append("\n");
733                 }
734                 result.append(horizontalLine);
735                 result.append("\n");
736                 left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1)
737                         .split("\\n");
738                 right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1)
739                         .split("\\n");
740                 for (int i = 0; i < left.length; i++)
741                 {
742                     result.append(left[i]);
743                     result.append(null == qtn ? SPACE : VLINE);
744                     result.append(right[i]);
745                     result.append("\n");
746                 }
747                 result.append("\n");
748             }
749             return result.toString();
750         }
751 
752         /**
753          * Return a String depicting this QuadTreeNode.
754          * @param recursionDepth int; levels to recurse
755          * @return String
756          */
757         public final String toStringGraphic(final int recursionDepth)
758         {
759             return subStringGraphic(this, recursionDepth);
760         }
761 
762     }
763 
764 }