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