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.draw.bounds.Bounds2d;
11 import org.djutils.draw.line.Polygon2d;
12 import org.djutils.draw.point.Point2d;
13 import org.djutils.exceptions.Throw;
14 import org.djutils.logger.CategoryLogger;
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 public class Ots2dSet implements Set<Polygon2d>, Serializable
36 {
37
38 private static final long serialVersionUID = 20170400L;
39
40
41 private final Set<Polygon2d> allShapes = new LinkedHashSet<Polygon2d>();
42
43
44 private final double minimumCellSize;
45
46
47 private QuadTreeNode quadTree;
48
49
50
51
52
53
54
55
56
57 public Ots2dSet(final Bounds2d boundingBox, final double minimumCellSize) throws OtsGeometryException
58 {
59 Throw.when(null == boundingBox, NullPointerException.class, "The boundingBox may not be null");
60 Throw.when(boundingBox.getDeltaX() <= 0 || boundingBox.getDeltaY() <= 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
69 @Override
70 public final int size()
71 {
72 return this.allShapes.size();
73 }
74
75
76 @Override
77 public final boolean isEmpty()
78 {
79 return this.allShapes.isEmpty();
80 }
81
82
83 @Override
84 public final boolean contains(final Object o)
85 {
86 return this.allShapes.contains(o);
87 }
88
89
90 @Override
91 public final Iterator<Polygon2d> iterator()
92 {
93 return new QuadTreeIterator();
94 }
95
96
97 @Override
98 public final Object[] toArray()
99 {
100 return this.allShapes.toArray();
101 }
102
103
104 @Override
105 public final <T> T[] toArray(final T[] a)
106 {
107 return this.allShapes.toArray(a);
108 }
109
110
111 @Override
112 public final boolean add(final Polygon2d 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 CategoryLogger.always().error("add: ERROR object could not be added to the quad tree");
125 }
126 return this.allShapes.add(e);
127 }
128
129
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((Polygon2d) o))
138 {
139 CategoryLogger.always().error("remove: ERROR object could not be removed from the quad tree");
140 }
141 return true;
142 }
143
144
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
159 @Override
160 public final boolean addAll(final Collection<? extends Polygon2d> c)
161 {
162 boolean result = false;
163 for (Polygon2d s : c)
164 {
165 if (add(s))
166 {
167 result = true;
168 }
169 }
170 return result;
171 }
172
173
174 @Override
175 public final boolean retainAll(final Collection<?> c)
176 {
177 boolean result = false;
178 for (Iterator<Polygon2d> it = iterator(); it.hasNext();)
179 {
180 Polygon2d shape = it.next();
181 if (!c.contains(shape))
182 {
183 it.remove();
184 result = true;
185 }
186 }
187 return result;
188 }
189
190
191 @Override
192 public final boolean removeAll(final Collection<?> c)
193 {
194 boolean result = false;
195 for (Iterator<Polygon2d> it = iterator(); it.hasNext();)
196 {
197 Polygon2d shape = it.next();
198 if (c.contains(shape))
199 {
200 it.remove();
201 result = true;
202 }
203 }
204 return result;
205 }
206
207
208 @Override
209 public final void clear()
210 {
211 this.quadTree.clear();
212 this.allShapes.clear();
213 }
214
215
216
217
218
219
220 public final Set<Polygon2d> intersectingShapes(final Bounds2d rectangle)
221 {
222 return this.quadTree.intersectingShapes(rectangle);
223 }
224
225
226
227
228
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
237 @Override
238 public final String toString()
239 {
240 return toString(0);
241 }
242
243
244
245
246
247
248 public final Set<Polygon2d> intersectingShapes(final Polygon2d shape)
249 {
250 Bounds2d bounds = shape.getBounds();
251 Set<Polygon2d> result =
252 intersectingShapes(new Bounds2d(bounds.getMinX(), bounds.getMinY(), bounds.getDeltaX(), bounds.getDeltaY()));
253 for (Iterator<Polygon2d> 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<Polygon2d>, Serializable
277 {
278
279 private static final long serialVersionUID = 20170400L;
280
281
282 @SuppressWarnings("synthetic-access")
283 private final Iterator<Polygon2d> theIterator = Ots2dSet.this.allShapes.iterator();
284
285
286 private Polygon2d 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 Polygon2d 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 CategoryLogger.always().error("iterator.remove: ERROR: could not remove {} from the quad tree",
312 this.lastResult);
313 }
314 }
315
316
317 @Override
318 public String toString()
319 {
320 return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
321 }
322
323 }
324
325
326
327
328 class QuadTreeNode implements Serializable
329 {
330
331 private static final long serialVersionUID = 20170400L;
332
333
334 private Set<Polygon2d> shapes = new LinkedHashSet<Polygon2d>();
335
336
337 private final Bounds2d boundingBox;
338
339
340 private final Polygon2d boundingShape;
341
342
343
344
345
346 private final QuadTreeNode[] leaves;
347
348
349
350
351
352 @SuppressWarnings("synthetic-access")
353 QuadTreeNode(final Bounds2d boundingBox)
354 {
355 this.boundingBox = boundingBox;
356 this.boundingShape = rectangleShape(boundingBox);
357 this.leaves = boundingBox.getDeltaY() > Ots2dSet.this.minimumCellSize
358 || boundingBox.getDeltaX() > Ots2dSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
359 }
360
361
362
363
364
365
366 public Set<Polygon2d> intersectingShapes(final Bounds2d rectangle)
367 {
368 Set<Polygon2d> result = new LinkedHashSet<Polygon2d>();
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 (Polygon2d shape : this.shapes)
385 {
386 Polygon2d 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 Bounds2d 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 Polygon2d shape)
423 {
424 if (!this.boundingShape.intersects(shape))
425 {
426 return false;
427 }
428 for (Polygon2d 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 Polygon2d shape)
485 {
486 return this.boundingShape.intersects(shape);
487 }
488
489
490
491
492
493
494 private Polygon2d rectangleShape(final Bounds2d rectangle)
495 {
496 double left = rectangle.getMinX();
497 double bottom = rectangle.getMinY();
498 double right = rectangle.getMaxX();
499 double top = rectangle.getMaxY();
500 return new Polygon2d(new Point2d(left, bottom), new Point2d(right, bottom), new Point2d(right, top),
501 new Point2d(left, top));
502 }
503
504
505
506
507
508
509 public final boolean add(final Polygon2d shape)
510 {
511 if (!this.boundingShape.intersects(shape))
512 {
513 return false;
514 }
515 if ((null == this.leaves) || shape.contains(this.boundingBox))
516 {
517
518 return this.shapes.add(shape);
519 }
520
521 boolean result = false;
522 for (int index = 0; index < this.leaves.length; index++)
523 {
524 if (null == this.leaves[index])
525 {
526 double subWidth = this.boundingBox.getDeltaX() / 2;
527 double subHeight = this.boundingBox.getDeltaY() / 2;
528 if (0 == subWidth)
529 {
530
531 subWidth = this.boundingBox.getDeltaX();
532 }
533 if (0 == subHeight)
534 {
535
536 subHeight = this.boundingBox.getDeltaY();
537 }
538 double left = this.boundingBox.getMinX();
539 if (0 != index / 2)
540 {
541 left += subWidth;
542 }
543 double bottom = this.boundingBox.getMinY();
544 if (0 != index % 2)
545 {
546 bottom += subHeight;
547 }
548 Bounds2d subBox = new Bounds2d(left, left + subWidth, bottom, bottom + subHeight);
549 if (rectangleShape(subBox).intersects(shape))
550 {
551
552 this.leaves[index] = new QuadTreeNode(subBox);
553 if (this.leaves[index].add(shape))
554 {
555 result = true;
556 }
557 else
558 {
559 throw new Error("Cannot happen: new QuadTreeNode refused to add shape that intersects it");
560 }
561 }
562 }
563 else
564 {
565
566 if (this.leaves[index].add(shape))
567 {
568 result = true;
569 }
570 }
571 }
572 return result;
573 }
574
575
576
577
578
579
580
581 private String printLeaf(final int recursionDepth, final int index)
582 {
583 QuadTreeNode leaf = this.leaves[index];
584 if (null == leaf)
585 {
586 return "null";
587 }
588 if (recursionDepth > 0)
589 {
590 return leaf.toString(recursionDepth - 1);
591 }
592 int leafSize = leaf.shapes.size();
593 return leafSize + " shape" + (1 == leafSize ? "" : "s");
594 }
595
596
597
598
599
600
601 final String toString(final int recursionDepth)
602 {
603 return "QuadTreeNode [" + this.shapes.size() + ", bounds=[LB: " + this.boundingBox.getMinX() + ","
604 + this.boundingBox.getMinY() + ", RT: " + this.boundingBox.getMaxX() + "," + this.boundingBox.getMaxY()
605 + "], " + subNodes(recursionDepth) + ", local " + this.shapes.size()
606 + (1 == this.shapes.size() ? " shape" : " shapes") + "]";
607 }
608
609
610
611
612
613
614 private String subNodes(final int recursionDepth)
615 {
616 if (null == this.leaves)
617 {
618 return "cannot have leaves";
619 }
620 return "leaves=[LB: " + printLeaf(recursionDepth, 0) + ", RB: " + printLeaf(recursionDepth, 1) + ", LT: "
621 + printLeaf(recursionDepth, 2) + ", RT: " + printLeaf(recursionDepth, 3) + "]";
622 }
623
624
625 @Override
626 public final String toString()
627 {
628 return toString(0);
629 }
630
631
632
633
634
635
636
637 private String repeat(final int count, final String string)
638 {
639 StringBuilder result = new StringBuilder();
640 for (int i = 0; i < count; i++)
641 {
642 result.append(string);
643 }
644 return result.toString();
645 }
646
647
648 private static final String VLINE = "|";
649
650
651 private static final String HLINE = "-";
652
653
654 private static final String SPACE = " ";
655
656
657 private static final int NUMBERSIZE = 6;
658
659
660
661
662
663
664
665
666 private String subStringGraphic(final QuadTreeNode qtn, final int recursionDepth)
667 {
668 StringBuffer result = new StringBuffer();
669 if (0 == recursionDepth)
670 {
671 if (null == qtn)
672 {
673 result.append(repeat(NUMBERSIZE, SPACE));
674 }
675 else
676 {
677 String numberBuf = String.format("%d", size());
678 int spare = NUMBERSIZE - numberBuf.length();
679 int filled = 0;
680 while (filled < spare / 2)
681 {
682 result.append(SPACE);
683 filled++;
684 }
685 result.append(numberBuf);
686 while (filled < spare)
687 {
688 result.append(SPACE);
689 filled++;
690 }
691 result.append("\n");
692 return result.toString();
693 }
694 }
695 else
696 {
697 String[] left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1)
698 .split("\\n");
699 String[] right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1)
700 .split("\\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 = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1)
730 .split("\\n");
731 right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1)
732 .split("\\n");
733 for (int i = 0; i < left.length; i++)
734 {
735 result.append(left[i]);
736 result.append(null == qtn ? SPACE : VLINE);
737 result.append(right[i]);
738 result.append("\n");
739 }
740 result.append("\n");
741 }
742 return result.toString();
743 }
744
745
746
747
748
749
750 public final String toStringGraphic(final int recursionDepth)
751 {
752 return subStringGraphic(this, recursionDepth);
753 }
754
755 }
756
757 }