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