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 org.opentrafficsim.core.Throw;
10
11 import com.vividsolutions.jts.geom.Envelope;
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/node/13">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<OTSShape>; 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<OTSShape>; 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<OTSShape>; 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 }