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