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
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 public class OTS2DSet implements Set<OTSShape>, Serializable
35 {
36
37 private static final long serialVersionUID = 20170400L;
38
39
40 private final Set<OTSShape> allShapes = new HashSet<OTSShape>();
41
42
43 private final double minimumCellSize;
44
45
46 private QuadTreeNode quadTree;
47
48
49
50
51
52
53
54
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
68 @Override
69 public final int size()
70 {
71 return this.allShapes.size();
72 }
73
74
75 @Override
76 public final boolean isEmpty()
77 {
78 return this.allShapes.isEmpty();
79 }
80
81
82 @Override
83 public final boolean contains(final Object o)
84 {
85 return this.allShapes.contains(o);
86 }
87
88
89 @Override
90 public final Iterator<OTSShape> iterator()
91 {
92 return new QuadTreeIterator();
93 }
94
95
96 @Override
97 public final Object[] toArray()
98 {
99 return this.allShapes.toArray();
100 }
101
102
103 @Override
104 public final <T> T[] toArray(final T[] a)
105 {
106 return this.allShapes.toArray(a);
107 }
108
109
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
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
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
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
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
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
207 @Override
208 public final void clear()
209 {
210 this.quadTree.clear();
211 this.allShapes.clear();
212 }
213
214
215
216
217
218
219 public final Set<OTSShape> intersectingShapes(final Rectangle2D rectangle)
220 {
221 return this.quadTree.intersectingShapes(rectangle);
222 }
223
224
225
226
227
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
236 @Override
237 public final String toString()
238 {
239 return toString(0);
240 }
241
242
243
244
245
246
247 public final Set<OTSShape> intersectingShapes(final OTSShape shape)
248 {
249 Envelope envelope = shape.getEnvelope();
250 Set<OTSShape> result =
251 intersectingShapes(new Rectangle2D.Double(envelope.getMinX(), envelope.getMinY(), envelope.getWidth(),
252 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
265
266
267
268 public final String toStringGraphic(final int recursionDepth)
269 {
270 return this.quadTree.toStringGraphic(recursionDepth);
271 }
272
273
274
275
276 class QuadTreeIterator implements Iterator<OTSShape>, Serializable
277 {
278
279 private static final long serialVersionUID = 20170400L;
280
281
282 @SuppressWarnings("synthetic-access")
283 private final Iterator<OTSShape> theIterator = OTS2DSet.this.allShapes.iterator();
284
285
286 private OTSShape lastResult = null;
287
288
289 @Override
290 public final boolean hasNext()
291 {
292 return this.theIterator.hasNext();
293 }
294
295
296 @Override
297 public final OTSShape next()
298 {
299 this.lastResult = this.theIterator.next();
300 return this.lastResult;
301 }
302
303
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 System.err.println("iterator.remove: ERROR: could not remove " + this.lastResult + " from the quad tree");
312 }
313 }
314
315
316 @Override
317 public String toString()
318 {
319 return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
320 }
321
322 }
323
324
325
326
327 class QuadTreeNode implements Serializable
328 {
329
330 private static final long serialVersionUID = 20170400L;
331
332
333 private Set<OTSShape> shapes = new HashSet<OTSShape>();
334
335
336 private final Rectangle2D boundingBox;
337
338
339 private final OTSShape boundingShape;
340
341
342
343
344
345 private final QuadTreeNode[] leaves;
346
347
348
349
350
351 @SuppressWarnings("synthetic-access")
352 QuadTreeNode(final Rectangle2D boundingBox)
353 {
354 this.boundingBox = boundingBox;
355 this.boundingShape = rectangleShape(boundingBox);
356 this.leaves =
357 boundingBox.getWidth() > OTS2DSet.this.minimumCellSize
358 || boundingBox.getHeight() > OTS2DSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
359 }
360
361
362
363
364
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
397
398
399
400 private boolean intersects(final Rectangle2D rectangle)
401 {
402 return this.boundingBox.intersects(rectangle);
403 }
404
405
406
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
419
420
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;
448 }
449 }
450 }
451 }
452 return result;
453 }
454
455
456
457
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
481
482
483
484 public boolean intersects(final OTSShape shape)
485 {
486 return this.boundingShape.intersects(shape);
487 }
488
489
490
491
492
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 exception.printStackTrace();
508 return null;
509 }
510 }
511
512
513
514
515
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
526 return this.shapes.add(shape);
527 }
528
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
539 subWidth = this.boundingBox.getWidth();
540 }
541 if (0 == subHeight)
542 {
543
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
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
574 if (this.leaves[index].add(shape))
575 {
576 result = true;
577 }
578 }
579 }
580 return result;
581 }
582
583
584
585
586
587
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
606
607
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
619
620
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
633 @Override
634 public final String toString()
635 {
636 return toString(0);
637 }
638
639
640
641
642
643
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
656 private static final String VLINE = "|";
657
658
659 private static final String HLINE = "-";
660
661
662 private static final String SPACE = " ";
663
664
665 private static final int NUMBERSIZE = 6;
666
667
668
669
670
671
672
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 =
706 subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1).split(
707 "\\n");
708 String[] right =
709 subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1).split(
710 "\\n");
711 String horizontalLine = null;
712 for (int i = 0; i < left.length; i++)
713 {
714 if (0 == i)
715 {
716 StringBuilder line = new StringBuilder();
717 int width = left[0].length() + 1 + right[0].length();
718 if (null == qtn)
719 {
720 line.append(repeat(width, SPACE));
721 }
722 else
723 {
724 String numberBuf = String.format("%d", qtn.shapes.size());
725 int spare = width - numberBuf.length();
726 line.append(repeat(spare / 2, HLINE));
727 line.append(numberBuf);
728 line.append(repeat(spare - spare / 2, HLINE));
729 }
730 horizontalLine = line.toString();
731 }
732 result.append(left[i]);
733 result.append(null == qtn ? SPACE : VLINE);
734 result.append(right[i]);
735 result.append("\n");
736 }
737 result.append(horizontalLine);
738 result.append("\n");
739 left =
740 subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1).split(
741 "\\n");
742 right =
743 subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1).split(
744 "\\n");
745 for (int i = 0; i < left.length; i++)
746 {
747 result.append(left[i]);
748 result.append(null == qtn ? SPACE : VLINE);
749 result.append(right[i]);
750 result.append("\n");
751 }
752 result.append("\n");
753 }
754 return result.toString();
755 }
756
757
758
759
760
761
762 public final String toStringGraphic(final int recursionDepth)
763 {
764 return subStringGraphic(this, recursionDepth);
765 }
766
767 }
768
769 }