1 package org.djutils.draw.curve;
2
3 import java.util.ArrayList;
4 import java.util.Iterator;
5 import java.util.List;
6 import java.util.Map.Entry;
7 import java.util.NavigableMap;
8 import java.util.NavigableSet;
9 import java.util.Set;
10 import java.util.SortedSet;
11 import java.util.TreeMap;
12 import java.util.TreeSet;
13
14 import org.djutils.draw.Transform2d;
15 import org.djutils.draw.function.ContinuousPiecewiseLinearFunction;
16 import org.djutils.draw.line.PolyLine2d;
17 import org.djutils.draw.line.Ray2d;
18 import org.djutils.draw.point.DirectedPoint2d;
19 import org.djutils.draw.point.Point2d;
20 import org.djutils.exceptions.Throw;
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 public class BezierCubic2d extends Bezier2d implements Curve2d, OffsetCurve2d, Curvature
38 {
39
40
41 private static final double STRAIGHT = Math.PI / 36000;
42
43
44 private final DirectedPoint2d startPoint;
45
46
47 private final DirectedPoint2d endPoint;
48
49
50 private final double length;
51
52
53
54
55
56
57
58
59
60
61 public BezierCubic2d(final Point2d start, final Point2d control1, final Point2d control2, final Point2d end)
62 {
63 super(start, control1, control2, end);
64 this.startPoint = new DirectedPoint2d(start, control1);
65 this.endPoint = new DirectedPoint2d(end, control2.directionTo(end));
66 this.length = super.getLength();
67 }
68
69
70
71
72
73
74
75 public BezierCubic2d(final Point2d[] points)
76 {
77 this(checkArray(points)[0], points[1], points[2], points[3]);
78 }
79
80
81
82
83
84
85
86
87 private static Point2d[] checkArray(final Point2d[] points)
88 {
89 Throw.when(points.length != 4, IllegalArgumentException.class, "points must contain exactly 4 Point2d objects");
90 return points;
91 }
92
93
94
95
96
97
98
99
100
101
102
103 public BezierCubic2d(final Ray2d start, final Ray2d end)
104 {
105 this(start, end, 1.0);
106 }
107
108
109
110
111
112
113
114
115
116
117
118
119
120 public BezierCubic2d(final Ray2d start, final Ray2d end, final double shape)
121 {
122 this(start, end, shape, false);
123 }
124
125
126
127
128
129
130
131
132
133
134
135
136 public BezierCubic2d(final Ray2d start, final Ray2d end, final double shape, final boolean weighted)
137 {
138 this(createControlPoints(start, end, shape, weighted));
139 }
140
141
142 private static final Point2d UNIT_VECTOR2D = new Point2d(1, 0);
143
144
145
146
147
148
149
150
151
152
153
154
155
156 private static Point2d[] createControlPoints(final Ray2d start, final Ray2d end, final double shape, final boolean weighted)
157 {
158 Throw.whenNull(start, "start");
159 Throw.whenNull(end, "end");
160 Throw.when(start.distanceSquared(end) == 0, IllegalArgumentException.class,
161 "Cannot create control points if start and end points coincide");
162 Throw.whenNaN(shape, "shape");
163 Throw.when(shape <= 0 || Double.isInfinite(shape), IllegalArgumentException.class,
164 "shape must be a finite, positive value");
165
166 Point2d control1;
167 Point2d control2;
168 if (weighted)
169 {
170
171
172 double distance = shape * start.distance(end);
173 double dStart = start.distance(end.projectOrthogonalExtended(start));
174 double dEnd = end.distance(start.projectOrthogonalExtended(end));
175 double wStart = dStart / (dStart + dEnd);
176 double wEnd = dEnd / (dStart + dEnd);
177 control1 = new Transform2d().translate(start).rotation(start.dirZ).scale(distance * wStart, distance * wStart)
178 .transform(UNIT_VECTOR2D);
179
180 control2 = new Transform2d().translate(end).rotation(end.dirZ + Math.PI).scale(distance * wEnd, distance * wEnd)
181 .transform(UNIT_VECTOR2D);
182 }
183 else
184 {
185
186 double distance = shape * start.distance(end) / 2.0;
187 control1 = start.getLocation(distance);
188
189 control2 = end.getLocationExtended(-distance);
190
191 }
192 return new Point2d[] {start, control1, control2, end};
193 }
194
195 @Override
196 public Point2d getPoint(final double t)
197 {
198 return new Point2d(B3(t, this.x), B3(t, this.y));
199 }
200
201
202
203
204
205
206
207
208 @SuppressWarnings("checkstyle:methodname")
209 private static double B3(final double t, final double[] p)
210 {
211 double t2 = t * t;
212 double t3 = t2 * t;
213 double m = (1.0 - t);
214 double m2 = m * m;
215 double m3 = m2 * m;
216 return m3 * p[0] + 3.0 * t * m2 * p[1] + 3.0 * t2 * m * p[2] + t3 * p[3];
217 }
218
219
220
221
222
223 @Override
224 public double getStartCurvature()
225 {
226 return curvature(0.0);
227 }
228
229
230
231
232
233 @Override
234 public double getEndCurvature()
235 {
236 return curvature(1.0);
237 }
238
239
240
241
242
243 private SortedSet<Double> getRoots()
244 {
245 TreeSet<Double> roots = new TreeSet<>();
246 for (double[] dimension : new double[][] {this.x, this.y})
247 {
248
249 double a = 3.0 * (-dimension[0] + 3.0 * dimension[1] - 3.0 * dimension[2] + dimension[3]);
250 double b = 6.0 * (dimension[0] - 2.0 * dimension[1] + dimension[2]);
251 double c = 3.0 * (dimension[1] - dimension[0]);
252
253 double discriminant = b * b - 4.0 * a * c;
254 if (discriminant > 0)
255 {
256 double sqrtDiscriminant = Math.sqrt(discriminant);
257 double a2 = 2.0 * a;
258 roots.add((-b + sqrtDiscriminant) / a2);
259 roots.add((-b - sqrtDiscriminant) / a2);
260 }
261 }
262
263 return roots.subSet(0.0, false, 1.0, false);
264 }
265
266
267
268
269
270 private SortedSet<Double> getInflections()
271 {
272
273 Point2d[] aligned = new Point2d[4];
274 double ang = -Math.atan2(getY(3) - getY(0), getX(3) - getX(0));
275 double cosAng = Math.cos(ang);
276 double sinAng = Math.sin(ang);
277 for (int i = 0; i < 4; i++)
278 {
279 aligned[i] = new Point2d(cosAng * (getX(i) - getX(0)) - sinAng * (getY(i) - getY(0)),
280 sinAng * (getX(i) - getX(0)) + cosAng * (getY(i) - getY(0)));
281 }
282
283
284
285
286 double a = aligned[2].x * aligned[1].y;
287 double b = aligned[3].x * aligned[1].y;
288 double c = aligned[1].x * aligned[2].y;
289 double d = aligned[3].x * aligned[2].y;
290
291 double x = -3.0 * a + 2.0 * b + 3.0 * c - d;
292 double y = 3.0 * a - b - 3.0 * c;
293 double z = c - a;
294
295
296 TreeSet<Double> inflections = new TreeSet<>();
297 if (Math.abs(x) < 1.0e-6)
298 {
299 if (Math.abs(y) >= 1.0e-12)
300 {
301 inflections.add(-z / y);
302 }
303 }
304 else
305 {
306 double det = y * y - 4.0 * x * z;
307 double sq = Math.sqrt(det);
308 double d2 = 2 * x;
309 if (det >= 0.0 && Math.abs(d2) >= 1e-12)
310 {
311 inflections.add(-(y + sq) / d2);
312 inflections.add((sq - y) / d2);
313 }
314 }
315
316
317 return inflections.subSet(0.0, false, 1.0, false);
318 }
319
320
321
322
323
324
325 private SortedSet<Double> getOffsetT(final Set<Double> fractions)
326 {
327 TreeSet<Double> result = new TreeSet<>();
328 for (Double f : fractions)
329 {
330 if (f > 0.0 && f < 1.0)
331 {
332 result.add(getT(f * getLength()));
333 }
334 }
335 return result;
336 }
337
338
339
340
341
342
343 private SortedSet<Double> getOffsetT(final Iterable<ContinuousPiecewiseLinearFunction.TupleSt> fractions)
344 {
345 TreeSet<Double> result = new TreeSet<>();
346 for (ContinuousPiecewiseLinearFunction.TupleSt tuple : fractions)
347 {
348 double f = tuple.s();
349 if (f > 0.0 && f < 1.0)
350 {
351 result.add(getT(f * getLength()));
352 }
353 }
354 return result;
355 }
356
357
358
359
360
361
362
363 @Override
364 public double getT(final double position)
365 {
366 if (0.0 == position)
367 {
368 return 0.0;
369 }
370 if (this.length == position)
371 {
372 return 1.0;
373 }
374
375 double t0 = 0.0;
376 double t2 = 1.0;
377 double t1 = 0.5;
378 while (t2 > t0 + 1.0e-6)
379 {
380 t1 = (t2 + t0) / 2.0;
381 SplitBeziers parts = split(t1);
382 double len1 = parts.first.getLength();
383 if (len1 < position)
384 {
385 t0 = t1;
386 }
387 else
388 {
389 t2 = t1;
390 }
391 }
392 return t1;
393 }
394
395
396
397
398
399
400 private record SplitBeziers(BezierCubic2d first, BezierCubic2d remainder)
401 {
402 };
403
404
405
406
407
408
409
410 public SplitBeziers split(final double t)
411 {
412 Throw.when(t < 0.0 || t > 1.0, IllegalArgumentException.class, "t value should be in the range [0.0 ... 1.0].");
413
414 List<Point2d> p1 = new ArrayList<>();
415 List<Point2d> p2 = new ArrayList<>();
416 List<Point2d> all = new ArrayList<>();
417 for (int i = 0; i < size(); i++)
418 {
419 all.add(new Point2d(getX(i), getY(i)));
420 }
421 split0(t, all, p1, p2);
422 SplitBeziers result = new SplitBeziers(new BezierCubic2d(p1.get(0), p1.get(1), p1.get(2), p1.get(3)),
423 new BezierCubic2d(p2.get(3), p2.get(2), p2.get(1), p2.get(0)));
424 return result;
425 }
426
427
428
429
430
431
432
433
434 private void split0(final double t, final List<Point2d> p, final List<Point2d> p1, final List<Point2d> p2)
435 {
436 if (p.size() == 1)
437 {
438 p1.add(p.get(0));
439 p2.add(p.get(0));
440 }
441 else
442 {
443 List<Point2d> pNew = new ArrayList<>();
444 for (int i = 0; i < p.size() - 1; i++)
445 {
446 if (i == 0)
447 {
448 p1.add(p.get(i));
449 }
450 if (i == p.size() - 2)
451 {
452 p2.add(p.get(i + 1));
453 }
454 double t1 = 1.0 - t;
455 pNew.add(new Point2d(t1 * p.get(i).x + t * p.get(i + 1).x, t1 * p.get(i).y + t * p.get(i + 1).y));
456 }
457 split0(t, pNew, p1, p2);
458 }
459 }
460
461
462 private Bezier2d derivative = null;
463
464 @Override
465 public PolyLine2d toPolyLine(final Flattener2d flattener)
466 {
467 Throw.whenNull(flattener, "Flattener may not be null");
468 if (null == this.derivative)
469 {
470 this.derivative = derivative();
471 }
472 return flattener.flatten(this);
473 }
474
475
476
477
478
479
480
481
482 private NavigableMap<Double, BezierCubic2d> segments;
483
484
485 private ContinuousPiecewiseLinearFunction ofForSegments = null;
486
487
488
489
490
491 private void updateSegments(final ContinuousPiecewiseLinearFunction of)
492 {
493 Throw.whenNull(of, "Offsets may not be null");
494 if (of.equals(this.ofForSegments))
495 {
496 return;
497 }
498
499 double ang1 = Math.atan2(getY(1) - getY(0), getX(1) - getX(0));
500 double ang2 = Math.atan2(getY(3) - getY(1), getX(3) - getX(1));
501 double ang3 = Math.atan2(getY(3) - getY(2), getX(3) - getX(2));
502 boolean straight =
503 Math.abs(ang1 - ang2) < STRAIGHT && Math.abs(ang2 - ang3) < STRAIGHT && Math.abs(ang3 - ang1) < STRAIGHT;
504
505 this.segments = new TreeMap<>();
506
507
508 NavigableMap<Double, Boundary> splits0 = new TreeMap<>();
509 if (!straight)
510 {
511 getRoots().forEach((t) -> splits0.put(t, Boundary.ROOT));
512 getInflections().forEach((t) -> splits0.put(t, Boundary.INFLECTION));
513 }
514 getOffsetT(of).forEach((t) -> splits0.put(t, Boundary.KNOT));
515 NavigableMap<Double, Boundary> splits = splits0.subMap(1e-6, false, 1.0 - 1e-6, false);
516
517
518
519
520 NavigableSet<Double> fCrossSectionRemain = new TreeSet<>();
521 for (ContinuousPiecewiseLinearFunction.TupleSt tuple : of)
522 {
523 if (tuple.s() > 0.0)
524 {
525 fCrossSectionRemain.add(tuple.s());
526 }
527 }
528 double lengthTotal = getLength();
529 BezierCubic2d currentBezier = this;
530
531 double lengthSoFar = 0.0;
532
533 double sig = Math.signum((getY(1) - getY(0)) * (getX(2) - getX(0)) - (getX(1) - getX(0)) * (getY(2) - getY(0)));
534
535 Iterator<Double> typeIterator = splits.navigableKeySet().iterator();
536 double tStart = 0.0;
537 if (splits.isEmpty())
538 {
539 this.segments.put(tStart, currentBezier.offset(of, lengthSoFar, lengthTotal, sig, true));
540 }
541 while (typeIterator.hasNext())
542 {
543 double tInFull = typeIterator.next();
544 Boundary type = splits.get(tInFull);
545 double t;
546
547
548
549 if (type == Boundary.ROOT)
550 {
551 t = currentBezier.getRoots().first();
552 }
553 else if (type == Boundary.INFLECTION)
554 {
555 t = currentBezier.getInflections().first();
556 }
557 else
558 {
559 NavigableSet<Double> fCrossSection = new TreeSet<>();
560 double fSoFar = lengthSoFar / lengthTotal;
561 double fFirst = fCrossSectionRemain.pollFirst();
562 fCrossSection.add((fFirst - fSoFar) / (1.0 - fSoFar));
563 SortedSet<Double> offsets = currentBezier.getOffsetT(fCrossSection);
564 t = offsets.first();
565 }
566 if (t < 1e-10)
567 {
568 continue;
569 }
570
571
572 SplitBeziers parts = currentBezier.split(t);
573 BezierCubic2d segment = parts.first.offset(of, lengthSoFar, lengthTotal, sig, false);
574
575 this.segments.put(tStart, segment);
576
577
578 lengthSoFar += parts.first.getLength();
579 if (type == Boundary.INFLECTION)
580 {
581 sig = -sig;
582 }
583 tStart = tInFull;
584
585
586 if (!typeIterator.hasNext())
587 {
588 BezierCubic2d lastSegment = parts.remainder.offset(of, lengthSoFar, lengthTotal, sig, true);
589
590 this.segments.put(tStart, lastSegment);
591 }
592 else
593 {
594 currentBezier = parts.remainder;
595 }
596 }
597 this.segments.put(1.0, null);
598 this.ofForSegments = of;
599 }
600
601 @Override
602 public Point2d getPoint(final double fraction, final ContinuousPiecewiseLinearFunction of)
603 {
604 updateSegments(of);
605 Entry<Double, BezierCubic2d> entry;
606 double nextT;
607 if (fraction == 1.0)
608 {
609 entry = BezierCubic2d.this.segments.lowerEntry(fraction);
610 nextT = fraction;
611 }
612 else
613 {
614 entry = BezierCubic2d.this.segments.floorEntry(fraction);
615 nextT = BezierCubic2d.this.segments.higherKey(fraction);
616 }
617 double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
618 return entry.getValue().getPoint(t);
619 }
620
621 @Override
622 public double getDirection(final double fraction, final ContinuousPiecewiseLinearFunction of)
623 {
624 updateSegments(of);
625 Entry<Double, BezierCubic2d> entry = BezierCubic2d.this.segments.floorEntry(fraction);
626 if (entry.getValue() == null)
627 {
628
629 entry = BezierCubic2d.this.segments.lowerEntry(fraction);
630 Point2d derivativeBezier = entry.getValue().derivative().getPoint(1.0);
631 return Math.atan2(derivativeBezier.y, derivativeBezier.x);
632 }
633 Double nextT = BezierCubic2d.this.segments.higherKey(fraction);
634 if (nextT == null)
635 {
636 nextT = 1.0;
637 }
638 double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
639 Point2d derivativeBezier = entry.getValue().derivative().getPoint(t);
640 return Math.atan2(derivativeBezier.y, derivativeBezier.x);
641 }
642
643 @Override
644 public PolyLine2d toPolyLine(final OffsetFlattener2d flattener, final ContinuousPiecewiseLinearFunction of)
645 {
646 Throw.whenNull(of, "fld");
647 Throw.whenNull(flattener, "flattener");
648
649 return flattener.flatten(this, of);
650 }
651
652
653
654
655
656
657
658
659
660
661 private BezierCubic2d offset(final ContinuousPiecewiseLinearFunction offsets, final double lengthSoFar,
662 final double lengthTotal, final double sig, final boolean last)
663 {
664 double offsetStart = sig * offsets.get(lengthSoFar / lengthTotal);
665 double offsetEnd = sig * offsets.get((lengthSoFar + getLength()) / lengthTotal);
666
667 Point2d p1 = new Point2d(getX(0) - (getY(1) - getY(0)), getY(0) + (getX(1) - getX(0)));
668 Point2d p2 = new Point2d(getX(3) - (getY(2) - getY(3)), getY(3) + (getX(2) - getX(3)));
669 Point2d center = Point2d.intersectionOfLines(new Point2d(getX(0), getY(0)), p1, p2, new Point2d(getX(3), getY(3)));
670
671 if (center == null)
672 {
673
674
675 Point2d[] newBezierPoints = new Point2d[4];
676 double ang = Math.atan2(p1.y - getY(0), p1.x - getX(0));
677 double dxStart = Math.cos(ang) * offsetStart;
678 double dyStart = -Math.sin(ang) * offsetStart;
679 newBezierPoints[0] = new Point2d(getX(0) + dxStart, getY(0) + dyStart);
680 newBezierPoints[1] = new Point2d(getX(1) + dxStart, getY(1) + dyStart);
681 double dxEnd = Math.cos(ang) * offsetEnd;
682 double dyEnd = -Math.sin(ang) * offsetEnd;
683 newBezierPoints[2] = new Point2d(getX(2) + dxEnd, getY(2) + dyEnd);
684 newBezierPoints[3] = new Point2d(getX(3) + dxEnd, getY(3) + dyEnd);
685 return new BezierCubic2d(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
686 }
687
688
689 Point2d[] newBezierPoints = new Point2d[4];
690 double off = offsetStart;
691 for (int i = 0; i < 4; i = i + 3)
692 {
693 double dy = getY(i) - center.y;
694 double dx = getX(i) - center.x;
695 double ang = Math.atan2(dy, dx);
696 double len = Math.hypot(dx, dy) + off;
697 newBezierPoints[i] = new Point2d(center.x + len * Math.cos(ang), center.y + len * Math.sin(ang));
698 off = offsetEnd;
699 }
700
701
702 double ang = sig * Math.atan((offsetEnd - offsetStart) / getLength());
703 double cosAng = Math.cos(ang);
704 double sinAng = Math.sin(ang);
705 double dx = getX(1) - getX(0);
706 double dy = getY(1) - getY(0);
707 double dx1;
708 double dy1;
709 if (0.0 == lengthSoFar)
710 {
711
712 dx1 = dx;
713 dy1 = dy;
714 }
715 else
716 {
717
718 dx1 = cosAng * dx - sinAng * dy;
719 dy1 = sinAng * dx + cosAng * dy;
720 }
721 dx = getX(2) - getX(3);
722 dy = getY(2) - getY(3);
723 double dx2;
724 double dy2;
725 if (last)
726 {
727
728 dx2 = dx;
729 dy2 = dy;
730 }
731 else
732 {
733
734 dx2 = cosAng * dx - sinAng * dy;
735 dy2 = sinAng * dx + cosAng * dy;
736 }
737
738
739
740 Point2d cp2 = new Point2d(newBezierPoints[0].x + dx1, newBezierPoints[0].y + dy1);
741 newBezierPoints[1] = Point2d.intersectionOfLines(newBezierPoints[0], cp2, center, new Point2d(getX(1), getY(1)));
742 Point2d cp3 = new Point2d(newBezierPoints[3].x + dx2, newBezierPoints[3].y + dy2);
743 newBezierPoints[2] = Point2d.intersectionOfLines(newBezierPoints[3], cp3, center, new Point2d(getX(2), getY(2)));
744
745
746 return new BezierCubic2d(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
747 }
748
749 @Override
750 public double getLength()
751 {
752 return this.length;
753 }
754
755 @Override
756 public String toString()
757 {
758 return "BezierCubic2d [startPoint=" + this.startPoint + ", endPoint=" + this.endPoint + ", controlPoint1="
759 + new Point2d(getX(1), getY(1)) + ", controlPoint2=" + new Point2d(getX(2), getY(2)) + ", length=" + this.length
760 + ", startDirZ=" + getStartPoint().dirZ + " (" + getPoint(0).directionTo(getPoint(1)) + "), endir="
761 + getEndPoint().dirZ + " (" + getPoint(2).directionTo(getPoint(3)) + ")]";
762 }
763
764
765
766
767 private enum Boundary
768 {
769
770 ROOT,
771
772
773 INFLECTION,
774
775
776 KNOT
777 }
778
779 }