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