1 package org.opentrafficsim.road.gtu.operational;
2
3 import java.util.ArrayList;
4 import java.util.LinkedHashSet;
5 import java.util.List;
6 import java.util.Optional;
7 import java.util.Set;
8
9 import org.djunits.value.vdouble.scalar.Acceleration;
10 import org.djunits.value.vdouble.scalar.Angle;
11 import org.djunits.value.vdouble.scalar.Direction;
12 import org.djunits.value.vdouble.scalar.Duration;
13 import org.djunits.value.vdouble.scalar.Length;
14 import org.djutils.draw.curve.BezierCubic2d;
15 import org.djutils.draw.curve.Flattener2d;
16 import org.djutils.draw.curve.Flattener2d.MaxDeviationAndAngle;
17 import org.djutils.draw.point.DirectedPoint2d;
18 import org.djutils.draw.point.Point2d;
19 import org.djutils.exceptions.Throw;
20 import org.djutils.exceptions.Try;
21 import org.djutils.math.AngleUtil;
22 import org.opentrafficsim.base.DistancedObject;
23 import org.opentrafficsim.base.geometry.FractionalProjectionHelper.FractionalFallback;
24 import org.opentrafficsim.base.geometry.OtsGeometryUtil;
25 import org.opentrafficsim.base.geometry.OtsLine2d;
26 import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
27 import org.opentrafficsim.core.gtu.plan.operational.Segments;
28 import org.opentrafficsim.core.network.LateralDirectionality;
29 import org.opentrafficsim.road.gtu.LaneBasedGtu;
30 import org.opentrafficsim.road.gtu.LaneBookkeeping;
31 import org.opentrafficsim.road.network.Lane;
32 import org.opentrafficsim.road.network.LanePosition;
33
34
35
36
37
38
39
40
41
42
43
44
45
46 public final class LaneOperationalPlanBuilder
47 {
48
49
50 private static final Length SNAP = Length.ofSI(1e-3);
51
52
53 private static final Length LANE_WIDTH = Length.ofSI(3.5);
54
55
56 private static final double FLATTEN_ANGLE = Math.PI / 360.0;
57
58
59 private static final Flattener2d FLATTENER = new MaxDeviationAndAngle(0.01, FLATTEN_ANGLE);
60
61
62 private LaneOperationalPlanBuilder()
63 {
64
65 }
66
67
68
69
70
71
72
73
74
75
76 public static LaneBasedOperationalPlan buildRoamingPlan(final LaneBasedGtu gtu, final Acceleration acceleration,
77 final Duration duration, final Duration tManeuver)
78 {
79 gtu.setLaneChangeDirection(LateralDirectionality.NONE);
80 LanePosition nearestPosition = gtu.getRoamingPosition();
81 Length deviation = Length.ZERO;
82 boolean deviative = true;
83 OtsLine2d path = getPath(gtu, nearestPosition, acceleration, duration, tManeuver, deviation, deviative).path();
84 Duration now = gtu.getSimulator().getSimulatorTime();
85 Segments segments = Segments.off(gtu.getSpeed(), duration, acceleration);
86 return Try.assign(() -> new LaneBasedOperationalPlan(gtu, path, now, segments, deviative),
87 "Building roaming plan produced inconsistent LaneBasedOperationalPlan.");
88 }
89
90
91
92
93
94
95
96
97
98
99
100 public static LaneBasedOperationalPlan buildPlanFromSimplePlan(final LaneBasedGtu gtu,
101 final SimpleOperationalPlan simplePlan, final Duration tManeuver, final DistancedObject<Length> deviation)
102 {
103 Throw.when(gtu.getLane() == null, IllegalStateException.class,
104 "Requested to build plan from simple plan for roaming GTU.");
105
106 Duration now = gtu.getSimulator().getSimulatorTime();
107
108 if (gtu.getSpeed().lt(OperationalPlan.DRIFTING_SPEED)
109 && simplePlan.getAcceleration().lt(OperationalPlan.DRIFTING_ACCELERATION))
110 {
111 DirectedPoint2d location = gtu.getLocation();
112 Point2d next = OtsGeometryUtil.translatePoint(location, 1.0);
113 OtsLine2d path = new OtsLine2d(location, next);
114 LanePosition nearestPosition = gtu.getPosition();
115 boolean deviative = nearestPosition.getLocation().distance(gtu.getLocation()) > SNAP.si;
116 Segments segments = Segments.standStill(simplePlan.getDuration());
117 return Try.assign(() -> new LaneBasedOperationalPlan(gtu, path, now, segments, deviative),
118 "Building operational plan produced inconsistent LaneBasedOperationalPlan.");
119 }
120
121 boolean deviative = false;
122 LanePosition nearestPosition;
123 if (simplePlan.isLaneChange())
124 {
125 boolean start = gtu.getBookkeeping().equals(LaneBookkeeping.START)
126 || (gtu.getBookkeeping().isStartAndEdge() && gtu.getSpeed().lt(LaneBookkeeping.START_THRESHOLD));
127 if (gtu.getBookkeeping().equals(LaneBookkeeping.INSTANT) || start)
128 {
129
130 gtu.changeLaneInstantaneously(simplePlan.getLaneChangeDirection());
131 nearestPosition = gtu.getPosition();
132
133 deviative = start;
134 }
135 else
136 {
137 gtu.setLaneChangeDirection(simplePlan.getLaneChangeDirection());
138 deviative = true;
139 Lane lane = gtu.getPosition().lane().getAdjacentLane(simplePlan.getLaneChangeDirection(), gtu.getType())
140 .orElseThrow(() -> new IllegalStateException("Starting lane change without adjacent lane."));
141 double fraction = lane.getCenterLine().projectFractionalAt(lane.getLink().getStartNode().getHeading(),
142 lane.getLink().getEndNode().getHeading(), gtu.getLocation().x, gtu.getLocation().y,
143 FractionalFallback.ENDPOINT);
144 nearestPosition = new LanePosition(lane, lane.getLength().times(fraction));
145 }
146 }
147 else
148 {
149 gtu.setLaneChangeDirection(LateralDirectionality.NONE);
150 nearestPosition = gtu.getPosition();
151 deviative = deviative || nearestPosition.getLocation().distance(gtu.getLocation()) > SNAP.si;
152 }
153 Length deviationHorizon = Length.max(tManeuver.times(gtu.getSpeed()), gtu.getVehicleModel().getTurnRadius(gtu));
154 Length targetDeviation = deviation.distance().gt(deviationHorizon) ? Length.ZERO : deviation.object();
155 deviative = deviative || targetDeviation.abs().gt(SNAP);
156 PathResults pathResults = getPath(gtu, nearestPosition, simplePlan.getAcceleration(), simplePlan.getDuration(),
157 tManeuver, targetDeviation, deviative);
158 Segments segments = Segments.off(gtu.getSpeed(), simplePlan.getDuration(), simplePlan.getAcceleration());
159 boolean finalDeviative = deviative || pathResults.neededDeviation();
160 return Try.assign(() -> new LaneBasedOperationalPlan(gtu, pathResults.path(), now, segments, finalDeviative),
161 "Building operational plan produced inconsistent LaneBasedOperationalPlan.");
162 }
163
164
165
166
167
168
169
170
171
172
173
174
175
176 private static PathResults getPath(final LaneBasedGtu gtu, final LanePosition nearestPosition,
177 final Acceleration acceleration, final Duration timeStep, final Duration tManeuver, final Length deviation,
178 final boolean deviative)
179 {
180 Length laneGap = Length.ZERO;
181 HorizonSpace horizonSpace =
182 getHorizonSpace(gtu, nearestPosition, acceleration, timeStep, tManeuver, deviation, laneGap);
183 if (deviative)
184 {
185 return bezierToHorizon(gtu, nearestPosition, deviation, horizonSpace);
186 }
187 return getCenterLinePath(gtu, nearestPosition, horizonSpace.horizon(), acceleration, timeStep, tManeuver, deviation);
188 }
189
190
191
192
193
194
195
196
197
198
199 private static PathResults bezierToHorizon(final LaneBasedGtu gtu, final LanePosition nearestPosition,
200 final Length deviation, final HorizonSpace horizonSpace)
201 {
202 DirectedPoint2d target = getTargetPoint(gtu, nearestPosition, deviation, horizonSpace);
203 target = extrapolateToHorizon(gtu, target, horizonSpace.horizon());
204 return new PathResults(bezierToTarget(gtu.getLocation(), target), false);
205 }
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220 private static PathResults getCenterLinePath(final LaneBasedGtu gtu, final LanePosition nearestPosition,
221 final Length length, final Acceleration acceleration, final Duration timeStep, final Duration tManeuver,
222 final Length deviation)
223 {
224 Length startDistance = nearestPosition.position();
225 Lane lane = nearestPosition.lane();
226 List<Point2d> points = new ArrayList<>();
227 Point2d lastPoint = null;
228 double cumulDist = 0.0;
229 while (lane != null)
230 {
231 if (startDistance.lt(lane.getLength()))
232 {
233 OtsLine2d centerLine = startDistance.gt0() ? lane.getCenterLine().extract(startDistance, lane.getLength())
234 : lane.getCenterLine();
235 if (lastPoint != null && lastPoint.distance(centerLine.getFirst()) > SNAP.si)
236 {
237
238 Length laneGap = Length.ofSI(lastPoint.distance(centerLine.getFirst()));
239 HorizonSpace horizonSpace =
240 getHorizonSpace(gtu, nearestPosition, acceleration, timeStep, tManeuver, deviation, laneGap);
241 return new PathResults(bezierToHorizon(gtu, nearestPosition, deviation, horizonSpace).path(), true);
242 }
243 for (Point2d point : centerLine)
244 {
245 if (lastPoint != null)
246 {
247 double d = lastPoint.distance(point);
248 if (d < SNAP.si)
249 {
250 continue;
251 }
252 cumulDist += d;
253 }
254 points.add(point);
255 if (cumulDist >= length.si)
256 {
257 return new PathResults(new OtsLine2d(points), false);
258 }
259 lastPoint = point;
260 }
261 }
262 startDistance = Length.ZERO;
263 lane = gtu.getNextLaneForRoute(lane).orElse(null);
264 }
265
266 Point2d lastLastPoint = points.get(points.size() - 2);
267 double direction = lastLastPoint.directionTo(lastPoint);
268 double r = length.si - cumulDist + SNAP.si;
269 points.add(lastPoint.translate(r * Math.cos(direction), r * Math.sin(direction)));
270 return new PathResults(new OtsLine2d(points), false);
271 }
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303 private static HorizonSpace getHorizonSpace(final LaneBasedGtu gtu, final LanePosition nearestPosition,
304 final Acceleration acceleration, final Duration timeStep, final Duration tManeuver, final Length deviation,
305 final Length laneGap)
306 {
307 DirectedPoint2d nearestPoint = nearestPosition.getLocation();
308 double dx = nearestPoint.x - gtu.getLocation().x;
309 double dy = nearestPoint.y - gtu.getLocation().y;
310 double dCenterLine = Math.hypot(dx, dy);
311 double leftOfLane;
312 if (dCenterLine <= SNAP.si)
313 {
314 leftOfLane = 1.0;
315 }
316 else if (dy == 0.0)
317 {
318 leftOfLane = -Math.signum(dx);
319 }
320 else if (dx == 0.0)
321 {
322 leftOfLane = -Math.signum(dy);
323 }
324 else
325 {
326 leftOfLane = Math.signum(Math.cos(Math.sin(nearestPoint.dirZ) * dx - nearestPoint.dirZ) * dy);
327 }
328 Length distanceToNearest = Length.ofSI(Math.abs(deviation.si - leftOfLane * dCenterLine));
329
330
331 double fLatDeviation =
332 Math.sin(.5 * Math.PI * Math.min(1.0, Math.max(distanceToNearest.si, laneGap.si) / LANE_WIDTH.si));
333
334
335 double dDirection = Math.abs(AngleUtil.normalizeAroundZero(nearestPoint.dirZ - gtu.getLocation().dirZ));
336 double fDirection = Math.min(1, 4 * dDirection / Math.PI);
337
338
339
340 Direction dirToNearest = Direction.ofSI(Math.atan2(dy, dx));
341 double fToTarget = 0.0;
342 if (dCenterLine > SNAP.si)
343 {
344 double dToTarget = Math.abs(AngleUtil.normalizeAroundZero(dirToNearest.si - gtu.getLocation().dirZ));
345 fToTarget = Math.max(0, 2 - 4 * dToTarget / Math.PI);
346 }
347
348
349 double tHorizon = tManeuver.si * Math.max(Math.max(fLatDeviation, fDirection), fToTarget);
350
351
352 Length turnDiameter = gtu.getVehicleModel().getTurnRadius(gtu);
353 double rPlan;
354 if (acceleration.lt0() && gtu.getSpeed().si / -acceleration.si < timeStep.si)
355 {
356 double t = gtu.getSpeed().si / -acceleration.si;
357 rPlan = gtu.getSpeed().si * t + .5 * acceleration.si * t * t;
358 }
359 else
360 {
361 rPlan = gtu.getSpeed().si * timeStep.si + .5 * acceleration.si * timeStep.si * timeStep.si;
362 }
363 Length horizon = Length.ofSI(Math.max(Math.max(turnDiameter.si, rPlan), gtu.getSpeed().si * tHorizon));
364 return new HorizonSpace(distanceToNearest, dirToNearest, horizon, turnDiameter, nearestPoint);
365 }
366
367
368
369
370
371
372
373
374
375 private record HorizonSpace(Length distanceToNearest, Direction dirToNearest, Length horizon, Length turnDiameter,
376 DirectedPoint2d nearestPoint)
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418 private static DirectedPoint2d getTargetPoint(final LaneBasedGtu gtu, final LanePosition nearestPosition,
419 final Length deviation, final HorizonSpace horizonSpace)
420 {
421 if (horizonSpace.distanceToNearest().gt(horizonSpace.horizon()))
422 {
423
424 if (Math.abs(horizonSpace.dirToNearest().si - gtu.getLocation().dirZ) < Math.PI / 4.0)
425 {
426
427 return new DirectedPoint2d(
428 gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(horizonSpace.dirToNearest().si),
429 gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(horizonSpace.dirToNearest().si),
430 horizonSpace.dirToNearest().si);
431 }
432 else
433 {
434
435 double alpha = Math.PI / 2.0;
436 double alphaMin = gtu.getLocation().dirZ - alpha;
437 double alphaMax = gtu.getLocation().dirZ + alpha;
438
439 double x1 = gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(alphaMin);
440 double y1 = gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(alphaMin);
441 Point2d nearest1 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(new Point2d(x1, y1));
442 double dist1sq = Math.hypot(x1 - nearest1.x, y1 - nearest1.y);
443
444 double x2 = gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(alphaMax);
445 double y2 = gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(alphaMax);
446 Point2d nearest2 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(new Point2d(x2, y2));
447 double dist2sq = Math.hypot(x2 - nearest2.x, y2 - nearest2.y);
448
449 if (nearest1.directionTo(nearest2) < SNAP.si)
450 {
451
452 double dirToAdjustedTarget = Math.atan2(nearest1.y - gtu.getLocation().y, nearest1.x - gtu.getLocation().x);
453 return new DirectedPoint2d(nearest1, dirToAdjustedTarget);
454 }
455 if (dist1sq <= dist2sq)
456 {
457 return new DirectedPoint2d(x1, y1, gtu.getLocation().dirZ + Math.PI);
458 }
459 return new DirectedPoint2d(x2, y2, gtu.getLocation().dirZ - Math.PI);
460 }
461 }
462
463
464 double alpha = Math.PI / 4.0;
465 double rVehicle = horizonSpace.turnDiameter().si;
466 double rHorizon = horizonSpace.horizon().si;
467 if (rVehicle > .5 * rHorizon)
468 {
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504 alpha = Math.min(alpha, Math.atan(rHorizon / Math.sqrt(4 * rVehicle * rVehicle - rHorizon * rHorizon)));
505 }
506
507
508 LanePosition endPosition = getTargetLanePosition(gtu, nearestPosition, horizonSpace.horizon(), Angle.ofSI(alpha));
509 if (endPosition != null)
510 {
511 DirectedPoint2d targetPoint = endPosition.getLocation();
512 if (deviation.eq0())
513 {
514 return targetPoint;
515 }
516
517 return OtsGeometryUtil.offsetPoint(targetPoint, deviation.si);
518 }
519
520
521 double alphaMin = gtu.getLocation().dirZ - alpha;
522 double alphaMax = gtu.getLocation().dirZ + alpha;
523 Point2d pMin = new Point2d(gtu.getLocation().x + rHorizon * Math.cos(alphaMin),
524 gtu.getLocation().y + rHorizon * Math.sin(alphaMin));
525 Point2d nearest1 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(pMin);
526 double dist1sq = nearest1.distance(pMin);
527 Point2d pMax = new Point2d(gtu.getLocation().x + rHorizon * Math.cos(alphaMax),
528 gtu.getLocation().y + rHorizon * Math.sin(alphaMax));
529 Point2d nearest2 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(pMax);
530 double dist2sq = nearest2.distance(pMax);
531 if (nearest1.distance(nearest2) < SNAP.si)
532 {
533
534 return new DirectedPoint2d(nearest1, gtu.getLocation().directionTo(nearest1));
535 }
536 else if (dist1sq <= dist2sq)
537 {
538
539
540
541
542
543
544 return new DirectedPoint2d(nearest1, gtu.getLocation().dirZ - 2 * alpha);
545 }
546 return new DirectedPoint2d(nearest2, gtu.getLocation().dirZ + 2 * alpha);
547 }
548
549
550
551
552
553
554
555
556
557
558
559 private static LanePosition getTargetLanePosition(final LaneBasedGtu gtu, final LanePosition startPosition,
560 final Length horizon, final Angle viewport)
561 {
562 DirectedPoint2d loc0 = gtu.getLocation();
563 OtsLine2d laneCenter =
564 startPosition.lane().getCenterLine().extract(startPosition.position(), startPosition.lane().getLength());
565 Lane lane = startPosition.lane();
566 Set<Lane> coveredLanes = new LinkedHashSet<>();
567 double r2 = horizon.si * horizon.si;
568 double distCumulLane = startPosition.position().si;
569 while (!coveredLanes.contains(lane))
570 {
571 coveredLanes.add(lane);
572
573
574
575 if (Math.hypot(laneCenter.getFirst().x - loc0.x, laneCenter.getFirst().y - loc0.y) > horizon.si)
576 {
577 double alpha = Math.atan2(laneCenter.getFirst().y - loc0.y, laneCenter.getFirst().x - loc0.x) - loc0.dirZ;
578 return Math.abs(alpha) <= viewport.si ? new LanePosition(lane, Length.ZERO) : null;
579 }
580
581
582 for (int i = 0; i < laneCenter.size() - 1; i++)
583 {
584 double dx = laneCenter.getX(i + 1) - laneCenter.getX(i);
585 double dy = laneCenter.getY(i + 1) - laneCenter.getY(i);
586 double dr = Math.hypot(dx, dy);
587
588
589 double det = (laneCenter.getX(i) - loc0.x) * (laneCenter.getY(i + 1) - loc0.y)
590 - (laneCenter.getX(i + 1) - loc0.x) * (laneCenter.getY(i) - loc0.y);
591 double dr2 = dr * dr;
592 double det2 = det * det;
593 double discriminant = r2 * dr2 - det2;
594 if (discriminant >= 0.0)
595 {
596 double sgn = dy < 0.0 ? -1.0 : 1.0;
597 double sqrtDisc = Math.sqrt(discriminant);
598
599 double pointSign = 1.0;
600 for (int j = 0; j < 2; j++)
601 {
602 double xP = loc0.x + (det * dy + pointSign * sgn * dx * sqrtDisc) / dr2;
603 double yP = loc0.y + (-det * dx + pointSign * Math.abs(dy) * sqrtDisc) / dr2;
604 double alpha = AngleUtil.normalizeAroundZero(Math.atan2(yP - loc0.y, xP - loc0.x) - loc0.dirZ);
605
606 double f = Math.abs(dx) > 0.0 && Math.abs(dx) > Math.abs(dy) ? (xP - laneCenter.getX(i)) / dx
607 : (dy == 0.0 ? 0.0 : (yP - laneCenter.getY(i)) / dy);
608
609 if (f >= 0.0 && f <= 1.0)
610 {
611
612 if (Math.abs(alpha) <= viewport.si)
613 {
614 return new LanePosition(lane, Length.ofSI(distCumulLane + f * dr));
615 }
616 else
617 {
618
619
620 return getTargetLanePosition(gtu, startPosition, horizon.times(2.0), Angle.ofSI(Math.PI / 4.0));
621 }
622 }
623 pointSign = -1.0;
624 }
625 }
626 distCumulLane += dr;
627 }
628
629
630 Optional<Lane> nextLane = gtu.getNextLaneForRoute(lane);
631 if (nextLane.isEmpty())
632 {
633 return new LanePosition(lane, Length.ofSI(startPosition.lane().getCenterLine().getLength()));
634 }
635 lane = nextLane.get();
636 laneCenter = lane.getCenterLine();
637 distCumulLane = 0.0;
638 }
639
640
641 double alpha = AngleUtil
642 .normalizeAroundZero(Math.atan2(laneCenter.getLast().y - loc0.y, laneCenter.getLast().x - loc0.x) - loc0.dirZ);
643 return Math.abs(alpha) <= viewport.si ? new LanePosition(lane, lane.getLength()) : null;
644 }
645
646
647
648
649
650
651
652
653
654 private static DirectedPoint2d extrapolateToHorizon(final LaneBasedGtu gtu, final DirectedPoint2d target,
655 final Length horizon)
656 {
657 double dx = target.x - gtu.getLocation().x;
658 double dy = target.y - gtu.getLocation().y;
659 double dist = Math.hypot(dx, dy);
660 if (dist >= horizon.si)
661 {
662 return target;
663 }
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682 double cosTarget = Math.cos(target.dirZ);
683 double sinTarget = Math.sin(target.dirZ);
684 double c = cosTarget * dx;
685 double s = sinTarget * dy;
686 double d = Math.sqrt(
687 -cosTarget * cosTarget * dy * dy + 2.0 * c * s - sinTarget * sinTarget * dx * dx + horizon.si * horizon.si);
688 double x = Math.max(-c - s - d, -c - s + d);
689 return new DirectedPoint2d(target.x + x * cosTarget, target.y + x * sinTarget, target.dirZ);
690 }
691
692
693
694
695
696
697
698
699 public static OtsLine2d bezierToTarget(final DirectedPoint2d from, final DirectedPoint2d target)
700 {
701 double angleShift = Math.abs(AngleUtil.normalizeAroundZero(from.dirZ - target.dirZ));
702 double dirToTarget = from.directionTo(target);
703 if (angleShift < FLATTEN_ANGLE && Math.abs(AngleUtil.normalizeAroundZero(dirToTarget - target.dirZ)) < FLATTEN_ANGLE
704 && Math.abs(AngleUtil.normalizeAroundZero(dirToTarget - from.dirZ)) < FLATTEN_ANGLE)
705 {
706
707 return new OtsLine2d(from, target);
708 }
709
710
711
712 double shapeFactor = (1.0 + 2.0 * Math.max(0.0, angleShift - 0.5 * Math.PI) / Math.PI) / 3;
713 double rControl = shapeFactor * Math.hypot(from.x - target.x, from.y - target.y);
714 Point2d p2 = OtsGeometryUtil.translatePoint(from, rControl);
715 Point2d p3 = OtsGeometryUtil.translatePoint(target, -rControl);
716 BezierCubic2d bezier = new BezierCubic2d(from, p2, p3, target);
717 return new OtsLine2d(bezier.toPolyLine(FLATTENER));
718 }
719
720
721
722
723
724
725 private record PathResults(OtsLine2d path, boolean neededDeviation)
726 {
727 };
728
729 }