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