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