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.djutils.logger.CategoryLogger;
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 public class Ots2dSet implements Set<OtsShape>, Serializable
33 {
34
35 private static final long serialVersionUID = 20170400L;
36
37
38 private final Set<OtsShape> allShapes = new LinkedHashSet<OtsShape>();
39
40
41 private final double minimumCellSize;
42
43
44 private QuadTreeNode quadTree;
45
46
47
48
49
50
51
52
53
54 public Ots2dSet(final Rectangle2D boundingBox, final double minimumCellSize) throws OtsGeometryException
55 {
56 Throw.when(null == boundingBox, NullPointerException.class, "The boundingBox may not be null");
57 Throw.when(boundingBox.getWidth() <= 0 || boundingBox.getHeight() <= 0, OtsGeometryException.class,
58 "The boundingBox must have nonzero surface (got %s", boundingBox);
59 Throw.when(minimumCellSize <= 0, OtsGeometryException.class, "The minimumCellSize must be > 0 (got %f)",
60 minimumCellSize);
61 this.quadTree = new QuadTreeNode(boundingBox);
62 this.minimumCellSize = minimumCellSize;
63 }
64
65
66 @Override
67 public final int size()
68 {
69 return this.allShapes.size();
70 }
71
72
73 @Override
74 public final boolean isEmpty()
75 {
76 return this.allShapes.isEmpty();
77 }
78
79
80 @Override
81 public final boolean contains(final Object o)
82 {
83 return this.allShapes.contains(o);
84 }
85
86
87 @Override
88 public final Iterator<OtsShape> iterator()
89 {
90 return new QuadTreeIterator();
91 }
92
93
94 @Override
95 public final Object[] toArray()
96 {
97 return this.allShapes.toArray();
98 }
99
100
101 @Override
102 public final <T> T[] toArray(final T[] a)
103 {
104 return this.allShapes.toArray(a);
105 }
106
107
108 @Override
109 public final boolean add(final OtsShape e)
110 {
111 if (!this.quadTree.intersects(e))
112 {
113 return false;
114 }
115 if (this.allShapes.contains(e))
116 {
117 return false;
118 }
119 if (!this.quadTree.add(e))
120 {
121 CategoryLogger.always().error("add: ERROR object could not be added to the quad tree");
122 }
123 return this.allShapes.add(e);
124 }
125
126
127 @Override
128 public final boolean remove(final Object o)
129 {
130 if (!this.allShapes.remove(o))
131 {
132 return false;
133 }
134 if (!this.quadTree.remove((OtsShape) o))
135 {
136 CategoryLogger.always().error("remove: ERROR object could not be removed from the quad tree");
137 }
138 return true;
139 }
140
141
142 @Override
143 public final boolean containsAll(final Collection<?> c)
144 {
145 for (Object o : c)
146 {
147 if (!contains(o))
148 {
149 return false;
150 }
151 }
152 return true;
153 }
154
155
156 @Override
157 public final boolean addAll(final Collection<? extends OtsShape> c)
158 {
159 boolean result = false;
160 for (OtsShape s : c)
161 {
162 if (add(s))
163 {
164 result = true;
165 }
166 }
167 return result;
168 }
169
170
171 @Override
172 public final boolean retainAll(final Collection<?> c)
173 {
174 boolean result = false;
175 for (Iterator<OtsShape> it = iterator(); it.hasNext();)
176 {
177 OtsShape shape = it.next();
178 if (!c.contains(shape))
179 {
180 it.remove();
181 result = true;
182 }
183 }
184 return result;
185 }
186
187
188 @Override
189 public final boolean removeAll(final Collection<?> c)
190 {
191 boolean result = false;
192 for (Iterator<OtsShape> it = iterator(); it.hasNext();)
193 {
194 OtsShape shape = it.next();
195 if (c.contains(shape))
196 {
197 it.remove();
198 result = true;
199 }
200 }
201 return result;
202 }
203
204
205 @Override
206 public final void clear()
207 {
208 this.quadTree.clear();
209 this.allShapes.clear();
210 }
211
212
213
214
215
216
217 public final Set<OtsShape> intersectingShapes(final Rectangle2D rectangle)
218 {
219 return this.quadTree.intersectingShapes(rectangle);
220 }
221
222
223
224
225
226
227 final String toString(final int recursionDepth)
228 {
229 return "Ots2dSet [contains " + size() + (1 == this.allShapes.size() ? "shape" : "shapes") + ", minimumCellSize="
230 + this.minimumCellSize + ", quadTree=" + this.quadTree.toString(recursionDepth) + "]";
231 }
232
233
234 @Override
235 public final String toString()
236 {
237 return toString(0);
238 }
239
240
241
242
243
244
245 public final Set<OtsShape> intersectingShapes(final OtsShape shape)
246 {
247 Bounds envelope = shape.getEnvelope();
248 Set<OtsShape> result = intersectingShapes(
249 new Rectangle2D.Double(envelope.getMinX(), envelope.getMinY(), envelope.getDeltaX(), envelope.getDeltaY()));
250 for (Iterator<OtsShape> it = result.iterator(); it.hasNext();)
251 {
252 if (!it.next().intersects(shape))
253 {
254 it.remove();
255 }
256 }
257 return result;
258 }
259
260
261
262
263
264
265 public final String toStringGraphic(final int recursionDepth)
266 {
267 return this.quadTree.toStringGraphic(recursionDepth);
268 }
269
270
271
272
273 class QuadTreeIterator implements Iterator<OtsShape>, Serializable
274 {
275
276 private static final long serialVersionUID = 20170400L;
277
278
279 @SuppressWarnings("synthetic-access")
280 private final Iterator<OtsShape> theIterator = Ots2dSet.this.allShapes.iterator();
281
282
283 private OtsShape lastResult = null;
284
285
286 @Override
287 public final boolean hasNext()
288 {
289 return this.theIterator.hasNext();
290 }
291
292
293 @Override
294 public final OtsShape next()
295 {
296 this.lastResult = this.theIterator.next();
297 return this.lastResult;
298 }
299
300
301 @SuppressWarnings("synthetic-access")
302 @Override
303 public final void remove()
304 {
305 this.theIterator.remove();
306 if (!Ots2dSet.this.quadTree.remove(this.lastResult))
307 {
308 CategoryLogger.always().error("iterator.remove: ERROR: could not remove {} from the quad tree",
309 this.lastResult);
310 }
311 }
312
313
314 @Override
315 public String toString()
316 {
317 return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
318 }
319
320 }
321
322
323
324
325 class QuadTreeNode implements Serializable
326 {
327
328 private static final long serialVersionUID = 20170400L;
329
330
331 private Set<OtsShape> shapes = new LinkedHashSet<OtsShape>();
332
333
334 private final Rectangle2D boundingBox;
335
336
337 private final OtsShape boundingShape;
338
339
340
341
342
343 private final QuadTreeNode[] leaves;
344
345
346
347
348
349 @SuppressWarnings("synthetic-access")
350 QuadTreeNode(final Rectangle2D boundingBox)
351 {
352 this.boundingBox = boundingBox;
353 this.boundingShape = rectangleShape(boundingBox);
354 this.leaves = boundingBox.getWidth() > Ots2dSet.this.minimumCellSize
355 || boundingBox.getHeight() > Ots2dSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
356 }
357
358
359
360
361
362
363 public Set<OtsShape> intersectingShapes(final Rectangle2D rectangle)
364 {
365 Set<OtsShape> result = new LinkedHashSet<OtsShape>();
366 if (!this.boundingBox.intersects(rectangle))
367 {
368 return result;
369 }
370 if (null == this.leaves)
371 {
372 return result;
373 }
374 for (QuadTreeNode leaf : this.leaves)
375 {
376 if (null != leaf && leaf.intersects(rectangle))
377 {
378 result.addAll(leaf.intersectingShapes(rectangle));
379 }
380 }
381 for (OtsShape shape : this.shapes)
382 {
383 OtsShape rectangleShape = rectangleShape(rectangle);
384 if (rectangleShape.intersects(shape))
385 {
386 result.add(shape);
387 }
388 }
389 return result;
390 }
391
392
393
394
395
396
397 private boolean intersects(final Rectangle2D rectangle)
398 {
399 return this.boundingBox.intersects(rectangle);
400 }
401
402
403
404
405 public void clear()
406 {
407 this.shapes.clear();
408 for (int index = 0; index < this.leaves.length; index++)
409 {
410 this.leaves[index] = null;
411 }
412 }
413
414
415
416
417
418
419 public boolean remove(final OtsShape shape)
420 {
421 if (!this.boundingShape.intersects(shape))
422 {
423 return false;
424 }
425 for (OtsShape s : this.shapes)
426 {
427 if (shape.equals(s))
428 {
429 this.shapes.remove(shape);
430 return true;
431 }
432 }
433 boolean result = false;
434 for (int index = 0; index < this.leaves.length; index++)
435 {
436 QuadTreeNode qtn = this.leaves[index];
437 if (null != qtn)
438 {
439 if (qtn.remove(shape))
440 {
441 result = true;
442 if (qtn.isEmpty())
443 {
444 this.leaves[index] = null;
445 }
446 }
447 }
448 }
449 return result;
450 }
451
452
453
454
455
456 private boolean isEmpty()
457 {
458 if (!this.shapes.isEmpty())
459 {
460 return false;
461 }
462 if (null == this.leaves)
463 {
464 return true;
465 }
466 for (QuadTreeNode qtn : this.leaves)
467 {
468 if (null != qtn)
469 {
470 return false;
471 }
472 }
473 return true;
474 }
475
476
477
478
479
480
481 public boolean intersects(final OtsShape shape)
482 {
483 return this.boundingShape.intersects(shape);
484 }
485
486
487
488
489
490
491 private OtsShape rectangleShape(final Rectangle2D rectangle)
492 {
493 double left = rectangle.getMinX();
494 double bottom = rectangle.getMinY();
495 double right = rectangle.getMaxX();
496 double top = rectangle.getMaxY();
497 try
498 {
499 return new OtsShape(new OtsPoint3d(left, bottom), new OtsPoint3d(right, bottom), new OtsPoint3d(right, top),
500 new OtsPoint3d(left, top));
501 }
502 catch (OtsGeometryException exception)
503 {
504 CategoryLogger.always().error(exception);
505 return null;
506 }
507 }
508
509
510
511
512
513
514 public final boolean add(final OtsShape shape)
515 {
516 if (!this.boundingShape.intersects(shape))
517 {
518 return false;
519 }
520 if ((null == this.leaves) || shape.contains(this.boundingBox))
521 {
522
523 return this.shapes.add(shape);
524 }
525
526 boolean result = false;
527 for (int index = 0; index < this.leaves.length; index++)
528 {
529 if (null == this.leaves[index])
530 {
531 double subWidth = this.boundingBox.getWidth() / 2;
532 double subHeight = this.boundingBox.getHeight() / 2;
533 if (0 == subWidth)
534 {
535
536 subWidth = this.boundingBox.getWidth();
537 }
538 if (0 == subHeight)
539 {
540
541 subHeight = this.boundingBox.getHeight();
542 }
543 double left = this.boundingBox.getMinX();
544 if (0 != index / 2)
545 {
546 left += subWidth;
547 }
548 double bottom = this.boundingBox.getMinY();
549 if (0 != index % 2)
550 {
551 bottom += subHeight;
552 }
553 Rectangle2D subBox = new Rectangle2D.Double(left, bottom, subWidth, subHeight);
554 if (rectangleShape(subBox).intersects(shape))
555 {
556
557 this.leaves[index] = new QuadTreeNode(subBox);
558 if (this.leaves[index].add(shape))
559 {
560 result = true;
561 }
562 else
563 {
564 throw new Error("Cannot happen: new QuadTreeNode refused to add shape that intersects it");
565 }
566 }
567 }
568 else
569 {
570
571 if (this.leaves[index].add(shape))
572 {
573 result = true;
574 }
575 }
576 }
577 return result;
578 }
579
580
581
582
583
584
585
586 private String printLeaf(final int recursionDepth, final int index)
587 {
588 QuadTreeNode leaf = this.leaves[index];
589 if (null == leaf)
590 {
591 return "null";
592 }
593 if (recursionDepth > 0)
594 {
595 return leaf.toString(recursionDepth - 1);
596 }
597 int leafSize = leaf.shapes.size();
598 return leafSize + " shape" + (1 == leafSize ? "" : "s");
599 }
600
601
602
603
604
605
606 final String toString(final int recursionDepth)
607 {
608 return "QuadTreeNode [" + this.shapes.size() + ", bounds=[LB: " + this.boundingBox.getMinX() + ","
609 + this.boundingBox.getMinY() + ", RT: " + this.boundingBox.getMaxX() + "," + this.boundingBox.getMaxY()
610 + "], " + subNodes(recursionDepth) + ", local " + this.shapes.size()
611 + (1 == this.shapes.size() ? " shape" : " shapes") + "]";
612 }
613
614
615
616
617
618
619 private String subNodes(final int recursionDepth)
620 {
621 if (null == this.leaves)
622 {
623 return "cannot have leaves";
624 }
625 return "leaves=[LB: " + printLeaf(recursionDepth, 0) + ", RB: " + printLeaf(recursionDepth, 1) + ", LT: "
626 + printLeaf(recursionDepth, 2) + ", RT: " + printLeaf(recursionDepth, 3) + "]";
627 }
628
629
630 @Override
631 public final String toString()
632 {
633 return toString(0);
634 }
635
636
637
638
639
640
641
642 private String repeat(final int count, final String string)
643 {
644 StringBuilder result = new StringBuilder();
645 for (int i = 0; i < count; i++)
646 {
647 result.append(string);
648 }
649 return result.toString();
650 }
651
652
653 private static final String VLINE = "|";
654
655
656 private static final String HLINE = "-";
657
658
659 private static final String SPACE = " ";
660
661
662 private static final int NUMBERSIZE = 6;
663
664
665
666
667
668
669
670
671 private String subStringGraphic(final QuadTreeNode qtn, final int recursionDepth)
672 {
673 StringBuffer result = new StringBuffer();
674 if (0 == recursionDepth)
675 {
676 if (null == qtn)
677 {
678 result.append(repeat(NUMBERSIZE, SPACE));
679 }
680 else
681 {
682 String numberBuf = String.format("%d", size());
683 int spare = NUMBERSIZE - numberBuf.length();
684 int filled = 0;
685 while (filled < spare / 2)
686 {
687 result.append(SPACE);
688 filled++;
689 }
690 result.append(numberBuf);
691 while (filled < spare)
692 {
693 result.append(SPACE);
694 filled++;
695 }
696 result.append("\n");
697 return result.toString();
698 }
699 }
700 else
701 {
702 String[] left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1)
703 .split("\\n");
704 String[] right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1)
705 .split("\\n");
706 String horizontalLine = null;
707 for (int i = 0; i < left.length; i++)
708 {
709 if (0 == i)
710 {
711 StringBuilder line = new StringBuilder();
712 int width = left[0].length() + 1 + right[0].length();
713 if (null == qtn)
714 {
715 line.append(repeat(width, SPACE));
716 }
717 else
718 {
719 String numberBuf = String.format("%d", qtn.shapes.size());
720 int spare = width - numberBuf.length();
721 line.append(repeat(spare / 2, HLINE));
722 line.append(numberBuf);
723 line.append(repeat(spare - spare / 2, HLINE));
724 }
725 horizontalLine = line.toString();
726 }
727 result.append(left[i]);
728 result.append(null == qtn ? SPACE : VLINE);
729 result.append(right[i]);
730 result.append("\n");
731 }
732 result.append(horizontalLine);
733 result.append("\n");
734 left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1)
735 .split("\\n");
736 right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1)
737 .split("\\n");
738 for (int i = 0; i < left.length; i++)
739 {
740 result.append(left[i]);
741 result.append(null == qtn ? SPACE : VLINE);
742 result.append(right[i]);
743 result.append("\n");
744 }
745 result.append("\n");
746 }
747 return result.toString();
748 }
749
750
751
752
753
754
755 public final String toStringGraphic(final int recursionDepth)
756 {
757 return subStringGraphic(this, recursionDepth);
758 }
759
760 }
761
762 }