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<OTSShape>; 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<OTSShape>; 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<OTSShape>; 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 }