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 com.vividsolutions.jts.geom.Envelope;
10
11 import nl.tudelft.simulation.language.Throw;
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 public class OTS2DSet implements Set<OTSShape>
34 {
35
36 private final Set<OTSShape> allShapes = new HashSet<OTSShape>();
37
38
39 private final double minimumCellSize;
40
41
42 private QuadTreeNode quadTree;
43
44
45
46
47
48
49
50
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
64 @Override
65 public final int size()
66 {
67 return this.allShapes.size();
68 }
69
70
71 @Override
72 public final boolean isEmpty()
73 {
74 return this.allShapes.isEmpty();
75 }
76
77
78 @Override
79 public final boolean contains(final Object o)
80 {
81 return this.allShapes.contains(o);
82 }
83
84
85 @Override
86 public final Iterator<OTSShape> iterator()
87 {
88 return new QuadTreeIterator();
89 }
90
91
92 @Override
93 public final Object[] toArray()
94 {
95 return this.allShapes.toArray();
96 }
97
98
99 @Override
100 public final <T> T[] toArray(final T[] a)
101 {
102 return this.allShapes.toArray(a);
103 }
104
105
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
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
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
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
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
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
203 @Override
204 public final void clear()
205 {
206 this.quadTree.clear();
207 this.allShapes.clear();
208 }
209
210
211
212
213
214
215 public final Set<OTSShape> intersectingShapes(final Rectangle2D rectangle)
216 {
217 return this.quadTree.intersectingShapes(rectangle);
218 }
219
220
221
222
223
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
232 @Override
233 public final String toString()
234 {
235 return toString(0);
236 }
237
238
239
240
241
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
261
262
263
264 public final String toStringGraphic(final int recursionDepth)
265 {
266 return this.quadTree.toStringGraphic(recursionDepth);
267 }
268
269
270
271
272 class QuadTreeIterator implements Iterator<OTSShape>
273 {
274
275 @SuppressWarnings("synthetic-access")
276 private final Iterator<OTSShape> theIterator = OTS2DSet.this.allShapes.iterator();
277
278
279 private OTSShape lastResult = null;
280
281
282 @Override
283 public final boolean hasNext()
284 {
285 return this.theIterator.hasNext();
286 }
287
288
289 @Override
290 public final OTSShape next()
291 {
292 this.lastResult = this.theIterator.next();
293 return this.lastResult;
294 }
295
296
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
309 @Override
310 public String toString()
311 {
312 return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
313 }
314
315 }
316
317
318
319
320 class QuadTreeNode
321 {
322
323 private Set<OTSShape> shapes = new HashSet<OTSShape>();
324
325
326 private final Rectangle2D boundingBox;
327
328
329 private final OTSShape boundingShape;
330
331
332
333
334
335 private final QuadTreeNode[] leaves;
336
337
338
339
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
353
354
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
387
388
389
390 private boolean intersects(final Rectangle2D rectangle)
391 {
392 return this.boundingBox.intersects(rectangle);
393 }
394
395
396
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
409
410
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;
438 }
439 }
440 }
441 }
442 return result;
443 }
444
445
446
447
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
471
472
473
474 public boolean intersects(final OTSShape shape)
475 {
476 return this.boundingShape.intersects(shape);
477 }
478
479
480
481
482
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
504
505
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
516 return this.shapes.add(shape);
517 }
518
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
529 subWidth = this.boundingBox.getWidth();
530 }
531 if (0 == subHeight)
532 {
533
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
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
564 if (this.leaves[index].add(shape))
565 {
566 result = true;
567 }
568 }
569 }
570 return result;
571 }
572
573
574
575
576
577
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
596
597
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
609
610
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
623 @Override
624 public final String toString()
625 {
626 return toString(0);
627 }
628
629
630
631
632
633
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
646 private static final String VLINE = "|";
647
648
649 private static final String HLINE = "-";
650
651
652 private static final String SPACE = " ";
653
654
655 private static final int NUMBERSIZE = 6;
656
657
658
659
660
661
662
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
749
750
751
752 public final String toStringGraphic(final int recursionDepth)
753 {
754 return subStringGraphic(this, recursionDepth);
755 }
756
757 }
758
759 }