1 package org.opentrafficsim.base.geometry;
2
3 import java.awt.geom.Line2D;
4 import java.awt.geom.Point2D;
5 import java.io.Serializable;
6 import java.util.ArrayList;
7 import java.util.Iterator;
8 import java.util.List;
9
10 import org.djunits.value.vdouble.scalar.Direction;
11 import org.djunits.value.vdouble.scalar.Length;
12 import org.djutils.draw.DrawRuntimeException;
13 import org.djutils.draw.line.PolyLine2d;
14 import org.djutils.draw.line.Ray2d;
15 import org.djutils.draw.point.OrientedPoint2d;
16 import org.djutils.draw.point.Point2d;
17 import org.djutils.exceptions.Throw;
18
19 import nl.tudelft.simulation.dsol.animation.Locatable;
20
21
22
23
24
25
26
27
28
29
30
31
32 public class OtsLine2d extends PolyLine2d implements Locatable, Serializable
33 {
34
35 private static final long serialVersionUID = 20150722L;
36
37
38 private final Length length;
39
40
41
42
43 private Point2d[] fractionalHelperCenters = null;
44
45
46 private Point2D.Double[] fractionalHelperDirections = null;
47
48
49 private Point2d firstOffsetIntersection;
50
51
52 private Point2d lastOffsetIntersection;
53
54
55 private static final double FRAC_PROJ_PRECISION = 2e-5 ;
56
57
58
59
60 private Length[] vertexRadii;
61
62
63
64
65
66 public OtsLine2d(final Point2d... points)
67 {
68 super(points);
69 this.length = Length.instantiateSI(lengthAtIndex(size() - 1));
70 }
71
72
73
74
75
76 public OtsLine2d(final PolyLine2d line2d)
77 {
78 super(line2d.getPoints());
79 this.length = Length.instantiateSI(lengthAtIndex(size() - 1));
80 }
81
82
83
84
85
86 public OtsLine2d(final Iterator<Point2d> line2d)
87 {
88 super(line2d);
89 this.length = Length.instantiateSI(lengthAtIndex(size() - 1));
90 }
91
92
93
94
95
96 public OtsLine2d(final List<Point2d> pointList)
97 {
98 super(pointList);
99 this.length = Length.instantiateSI(lengthAtIndex(size() - 1));
100 }
101
102
103
104
105
106
107 @Override
108 public final OtsLine2d offsetLine(final double offset)
109 {
110 return new OtsLine2d(super.offsetLine(offset));
111 }
112
113
114
115
116
117
118
119
120 @Override
121 public final OtsLine2d offsetLine(final double offsetAtStart, final double offsetAtEnd)
122 {
123 return new OtsLine2d(super.offsetLine(offsetAtStart, offsetAtEnd).getPointList());
124 }
125
126
127
128
129
130
131
132
133
134 public final OtsLine2d offsetLine(final double[] relativeFractions, final double[] offsets)
135 {
136 return new OtsLine2d(OtsGeometryUtil.offsetLine(this, relativeFractions, offsets));
137 }
138
139
140
141
142
143
144
145 public static OtsLine2d concatenate(final OtsLine2d... lines)
146 {
147 return concatenate(0.0, lines);
148 }
149
150
151
152
153
154
155
156
157 public static OtsLine2d concatenate(final double toleranceSI, final OtsLine2d line1, final OtsLine2d line2)
158 {
159 return new OtsLine2d(PolyLine2d.concatenate(toleranceSI, line1, line2));
160 }
161
162
163
164
165
166
167
168
169 public static OtsLine2d concatenate(final double toleranceSI, final OtsLine2d... lines)
170 {
171 List<PolyLine2d> lines2d = new ArrayList<>();
172 for (OtsLine2d line : lines)
173 {
174 lines2d.add(line);
175 }
176 return new OtsLine2d(PolyLine2d.concatenate(toleranceSI, lines2d.toArray(new PolyLine2d[lines.length])));
177 }
178
179
180
181
182
183 @Override
184 public final OtsLine2d reverse()
185 {
186 return new OtsLine2d(super.reverse());
187 }
188
189
190
191
192
193
194
195 @Override
196 public OtsLine2d extractFractional(final double start, final double end)
197 {
198 return extract(start * this.length.si, end * this.length.si);
199 }
200
201
202
203
204
205
206
207
208 public final OtsLine2d extract(final Length start, final Length end)
209 {
210 return extract(start.si, end.si);
211 }
212
213
214
215
216
217
218
219
220 @Override
221 public final OtsLine2d extract(final double start, final double end)
222 {
223 return new OtsLine2d(super.extract(start, end));
224 }
225
226
227
228
229
230
231 public final Length getTypedLength()
232 {
233 return this.length;
234 }
235
236
237
238
239
240
241
242 public final OrientedPoint2d getLocationExtended(final Length position)
243 {
244 return rayToPoint(getLocationExtended(position.si));
245 }
246
247
248
249
250
251
252
253 public final OrientedPoint2d getLocationExtendedSI(final double positionSI)
254 {
255 return rayToPoint(getLocationExtended(positionSI));
256 }
257
258
259
260
261
262
263
264 public final OrientedPoint2d getLocationPointFraction(final double fraction) throws DrawRuntimeException
265 {
266 return rayToPoint(getLocationFraction(fraction));
267 }
268
269
270
271
272
273
274
275
276 public final OrientedPoint2d getLocationPointFraction(final double fraction, final double tolerance)
277 throws DrawRuntimeException
278 {
279 return rayToPoint(getLocationFraction(fraction, tolerance));
280 }
281
282
283
284
285
286
287 public final OrientedPoint2d getLocationPointFractionExtended(final double fraction)
288 {
289 return rayToPoint(getLocationFractionExtended(fraction));
290 }
291
292
293
294
295
296
297
298 public final OrientedPoint2d getLocation(final Length position) throws DrawRuntimeException
299 {
300 return rayToPoint(getLocation(position.si));
301 }
302
303
304
305
306
307
308
309 public final OrientedPoint2d getLocationSI(final double positionSI) throws DrawRuntimeException
310 {
311 return rayToPoint(getLocation(positionSI));
312 }
313
314
315
316
317
318
319 private OrientedPoint2d rayToPoint(final Ray2d ray)
320 {
321 return new OrientedPoint2d(ray.x, ray.y, ray.phi);
322 }
323
324
325
326
327
328
329 @Override
330 public final OtsLine2d truncate(final double lengthSI)
331 {
332 return new OtsLine2d(super.truncate(lengthSI));
333 }
334
335
336
337
338
339
340
341
342 public final double projectOrthogonalSnap(final double x, final double y)
343 {
344 Point2d closest = closestPointOnPolyLine(new Point2d(x, y));
345 return projectOrthogonalFractionalExtended(closest);
346 }
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402 public final synchronized double projectFractional(final Direction start, final Direction end, final double x,
403 final double y, final FractionalFallback fallback)
404 {
405
406
407 double minDistance = Double.POSITIVE_INFINITY;
408 double minSegmentFraction = 0;
409 int minSegment = -1;
410 Point2d point = new Point2d(x, y);
411
412
413 determineFractionalHelpers(start, end);
414
415
416 double[] d = new double[size() - 1];
417 double minD = Double.POSITIVE_INFINITY;
418 for (int i = 0; i < size() - 1; i++)
419 {
420 d[i] = Line2D.ptSegDist(get(i).x, get(i).y, get(i + 1).x, get(i + 1).y, x, y);
421 minD = d[i] < minD ? d[i] : minD;
422 }
423
424
425 double distance;
426 for (int i = 0; i < size() - 1; i++)
427 {
428
429 if (d[i] > minD + FRAC_PROJ_PRECISION)
430 {
431 continue;
432 }
433 Point2d center = this.fractionalHelperCenters[i];
434 Point2d p;
435 if (center != null)
436 {
437
438 p = intersectionOfLines(center, point, get(i), get(i + 1));
439 if (p == null || (x < center.x + FRAC_PROJ_PRECISION && center.x + FRAC_PROJ_PRECISION < p.x)
440 || (x > center.x - FRAC_PROJ_PRECISION && center.x - FRAC_PROJ_PRECISION > p.x)
441 || (y < center.y + FRAC_PROJ_PRECISION && center.y + FRAC_PROJ_PRECISION < p.y)
442 || (y > center.y - FRAC_PROJ_PRECISION && center.y - FRAC_PROJ_PRECISION > p.y))
443 {
444
445 continue;
446 }
447 }
448 else
449 {
450
451 Point2d offsetPoint =
452 new Point2d(x + this.fractionalHelperDirections[i].x, y + this.fractionalHelperDirections[i].y);
453 p = intersectionOfLines(point, offsetPoint, get(i), get(i + 1));
454 }
455 double segLength = get(i).distance(get(i + 1)) + FRAC_PROJ_PRECISION;
456 if (p == null || get(i).distance(p) > segLength || get(i + 1).distance(p) > segLength)
457 {
458
459
460 continue;
461 }
462
463 double dx = x - p.x;
464 double dy = y - p.y;
465 distance = Math.hypot(dx, dy);
466
467 if (distance < minDistance)
468 {
469 dx = p.x - get(i).x;
470 dy = p.y - get(i).y;
471 double dFrac = Math.hypot(dx, dy);
472
473 minDistance = distance;
474 minSegmentFraction = dFrac / (lengthAtIndex(i + 1) - lengthAtIndex(i));
475 minSegment = i;
476 }
477 }
478
479
480 if (minSegment == -1)
481
482 {
483
484
485
486
487
488 return fallback.getFraction(this, x, y);
489 }
490
491 double segLen = lengthAtIndex(minSegment + 1) - lengthAtIndex(minSegment);
492 return (lengthAtIndex(minSegment) + segLen * minSegmentFraction) / this.length.si;
493
494 }
495
496
497
498
499
500
501
502
503
504
505
506
507 public enum FractionalFallback
508 {
509
510 ORTHOGONAL
511 {
512 @Override
513 double getFraction(final OtsLine2d line, final double x, final double y)
514 {
515 return line.projectOrthogonalSnap(x, y);
516 }
517 },
518
519
520 ENDPOINT
521 {
522 @Override
523 double getFraction(final OtsLine2d line, final double x, final double y)
524 {
525 Point2d point = new Point2d(x, y);
526 double dStart = point.distance(line.getFirst());
527 double dEnd = point.distance(line.getLast());
528 if (dStart < dEnd)
529 {
530 return -dStart / line.length.si;
531 }
532 else
533 {
534 return (dEnd + line.length.si) / line.length.si;
535 }
536 }
537 },
538
539
540 NaN
541 {
542 @Override
543 double getFraction(final OtsLine2d line, final double x, final double y)
544 {
545 return Double.NaN;
546 }
547 };
548
549
550
551
552
553
554
555
556 abstract double getFraction(OtsLine2d line, double x, double y);
557
558 }
559
560
561
562
563
564
565
566 private synchronized void determineFractionalHelpers(final Direction start, final Direction end)
567 {
568
569 final int n = size() - 1;
570
571
572 if (this.fractionalHelperCenters == null)
573 {
574 this.fractionalHelperCenters = new Point2d[n];
575 this.fractionalHelperDirections = new Point2D.Double[n];
576 if (size() > 2)
577 {
578
579 PolyLine2d prevOfsSeg = unitOffsetSegment(0);
580 PolyLine2d nextOfsSeg = unitOffsetSegment(1);
581 Point2d parStartPoint;
582 parStartPoint = intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0), nextOfsSeg.get(1));
583 if (parStartPoint == null || prevOfsSeg.get(1).distance(nextOfsSeg.get(0)) < Math
584 .min(prevOfsSeg.get(1).distance(parStartPoint), nextOfsSeg.get(0).distance(parStartPoint)))
585 {
586 parStartPoint = new Point2d((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
587 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
588 }
589
590 this.firstOffsetIntersection = parStartPoint;
591
592 for (int i = 1; i < size() - 2; i++)
593 {
594 prevOfsSeg = nextOfsSeg;
595 nextOfsSeg = unitOffsetSegment(i + 1);
596 Point2d parEndPoint;
597 parEndPoint =
598 intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0), nextOfsSeg.get(1));
599 if (parEndPoint == null || prevOfsSeg.get(1).distance(nextOfsSeg.get(0)) < Math
600 .min(prevOfsSeg.get(1).distance(parEndPoint), nextOfsSeg.get(0).distance(parEndPoint)))
601 {
602 parEndPoint = new Point2d((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
603 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
604 }
605
606 this.fractionalHelperCenters[i] = intersectionOfLines(get(i), parStartPoint, get(i + 1), parEndPoint);
607 if (this.fractionalHelperCenters[i] == null)
608 {
609
610 this.fractionalHelperDirections[i] =
611 new Point2D.Double(parStartPoint.x - get(i).x, parStartPoint.y - get(i).y);
612 }
613 parStartPoint = parEndPoint;
614 }
615
616 this.lastOffsetIntersection = parStartPoint;
617 }
618 }
619
620
621 double ang = (start == null ? Math.atan2(get(1).y - get(0).y, get(1).x - get(0).x) : start.si) + Math.PI / 2;
622 Point2d p1 = new Point2d(get(0).x + Math.cos(ang), get(0).y + Math.sin(ang));
623 ang = (end == null ? Math.atan2(get(n).y - get(n - 1).y, get(n).x - get(n - 1).x) : end.si) + Math.PI / 2;
624 Point2d p2 = new Point2d(get(n).x + Math.cos(ang), get(n).y + Math.sin(ang));
625
626
627 if (size() > 2)
628 {
629 this.fractionalHelperCenters[0] = intersectionOfLines(get(0), p1, get(1), this.firstOffsetIntersection);
630 this.fractionalHelperCenters[n - 1] = intersectionOfLines(get(n - 1), this.lastOffsetIntersection, get(n), p2);
631 if (this.fractionalHelperCenters[n - 1] == null)
632 {
633
634 this.fractionalHelperDirections[n - 1] = new Point2D.Double(p2.x - get(n).x, p2.y - get(n).y);
635 }
636 }
637 else
638 {
639
640 this.fractionalHelperCenters[0] = intersectionOfLines(get(0), p1, get(1), p2);
641 }
642 if (this.fractionalHelperCenters[0] == null)
643 {
644
645 this.fractionalHelperDirections[0] = new Point2D.Double(p1.x - get(0).x, p1.y - get(0).y);
646 }
647
648 }
649
650
651
652
653
654
655
656
657
658
659
660 private Point2d intersectionOfLines(final Point2d line1P1, final Point2d line1P2, final Point2d line2P1,
661 final Point2d line2P2)
662 {
663 double l1p1x = line1P1.x;
664 double l1p1y = line1P1.y;
665 double l1p2x = line1P2.x - l1p1x;
666 double l1p2y = line1P2.y - l1p1y;
667 double l2p1x = line2P1.x - l1p1x;
668 double l2p1y = line2P1.y - l1p1y;
669 double l2p2x = line2P2.x - l1p1x;
670 double l2p2y = line2P2.y - l1p1y;
671 double determinant = (0 - l1p2x) * (l2p1y - l2p2y) - (0 - l1p2y) * (l2p1x - l2p2x);
672 if (Math.abs(determinant) < 0.0000001)
673 {
674 return null;
675 }
676 return new Point2d(l1p1x + (l1p2x * (l2p1x * l2p2y - l2p1y * l2p2x)) / determinant,
677 l1p1y + (l1p2y * (l2p1x * l2p2y - l2p1y * l2p2x)) / determinant);
678 }
679
680
681
682
683
684
685 private synchronized PolyLine2d unitOffsetSegment(final int segment)
686 {
687 return new PolyLine2d(get(segment), get(segment + 1)).offsetLine(1.0);
688 }
689
690
691
692
693
694
695
696
697
698
699
700 public synchronized Length getProjectedRadius(final double fraction) throws IllegalArgumentException
701 {
702 Throw.when(fraction < 0.0 || fraction > 1.0, IllegalArgumentException.class,
703 "Fraction %f is out of bounds [0.0 ... 1.0]", fraction);
704 if (this.vertexRadii == null)
705 {
706 this.vertexRadii = new Length[size() - 1];
707 }
708 int index = find(fraction * getLength());
709 if (index > 0 && this.vertexRadii[index] == null)
710 {
711 this.vertexRadii[index] = getProjectedVertexRadius(index);
712 }
713 if (index < size() - 2 && this.vertexRadii[index + 1] == null)
714 {
715 this.vertexRadii[index + 1] = getProjectedVertexRadius(index + 1);
716 }
717 if (index == 0)
718 {
719 if (this.vertexRadii.length < 2)
720 {
721 return Length.instantiateSI(Double.NaN);
722 }
723 return this.vertexRadii[1];
724 }
725 if (index == size() - 2)
726 {
727 return this.vertexRadii[size() - 2];
728 }
729 return Math.abs(this.vertexRadii[index].si) < Math.abs(this.vertexRadii[index + 1].si) ? this.vertexRadii[index]
730 : this.vertexRadii[index + 1];
731 }
732
733
734
735
736
737
738
739
740
741
742 public synchronized Length getProjectedVertexRadius(final int index) throws IndexOutOfBoundsException
743 {
744 Throw.when(index < 1 || index > size() - 2, IndexOutOfBoundsException.class,
745 "Index %d is out of bounds [1 ... size() - 2].", index);
746 determineFractionalHelpers(null, null);
747 double length1 = lengthAtIndex(index) - lengthAtIndex(index - 1);
748 double length2 = lengthAtIndex(index + 1) - lengthAtIndex(index);
749 int shortIndex = length1 < length2 ? index : index + 1;
750
751 Point2d p1 =
752 new Point2d(.5 * (get(shortIndex - 1).x + get(shortIndex).x), .5 * (get(shortIndex - 1).y + get(shortIndex).y));
753
754 Point2d p2 = new Point2d(p1.x + (get(shortIndex).y - get(shortIndex - 1).y),
755 p1.y - (get(shortIndex).x - get(shortIndex - 1).x));
756
757 Point2d p3 = get(index);
758
759 Point2d p4 = this.fractionalHelperCenters[index];
760 if (p4 == null)
761 {
762
763 p4 = new Point2d(p3.x + this.fractionalHelperDirections[index].x, p3.y + this.fractionalHelperDirections[index].y);
764 }
765 Point2d intersection = intersectionOfLines(p1, p2, p3, p4);
766 if (null == intersection)
767 {
768 return Length.instantiateSI(Double.NaN);
769 }
770
771 double refLength = length1 < length2 ? length1 : length2;
772 double radius = intersection.distance(p1);
773 double i2p2 = intersection.distance(p2);
774 if (radius < i2p2 && i2p2 > refLength)
775 {
776
777 return Length.instantiateSI(radius);
778 }
779
780 return Length.instantiateSI(-radius);
781 }
782
783
784
785
786
787
788
789 public double getVertexFraction(final int index) throws IndexOutOfBoundsException
790 {
791 Throw.when(index < 0 || index > size() - 1, IndexOutOfBoundsException.class, "Index %d is out of bounds [0 %d].", index,
792 size() - 1);
793 return lengthAtIndex(index) / this.length.si;
794 }
795
796
797
798
799
800 public final Point2d getCentroid()
801 {
802 return getBounds().midPoint();
803 }
804
805 @Override
806 @SuppressWarnings("checkstyle:designforextension")
807 public Point2d getLocation()
808 {
809 return getCentroid();
810 }
811
812 }