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