View Javadoc
1   package org.opentrafficsim.road.gtu.lane.plan.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.geometry.OtsLine2d;
23  import org.opentrafficsim.base.geometry.OtsLine2d.FractionalFallback;
24  import org.opentrafficsim.core.gtu.plan.operational.Segments;
25  import org.opentrafficsim.core.network.LateralDirectionality;
26  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
27  import org.opentrafficsim.road.gtu.lane.LaneBookkeeping;
28  import org.opentrafficsim.road.network.lane.Lane;
29  import org.opentrafficsim.road.network.lane.LanePosition;
30  
31  /**
32   * Builder for several often used operational plans. E.g., decelerate to come to a full stop at the end of a shape; accelerate
33   * to reach a certain speed at the end of a curve; drive constant on a curve; decelerate or accelerate to reach a given end
34   * speed at the end of a curve, etc.<br>
35   * <p>
36   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
37   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
38   * </p>
39   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
40   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
41   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
42   */
43  public final class LaneOperationalPlanBuilder
44  {
45  
46      /** Length within which GTUs snap to the lane center line. */
47      private static final Length SNAP = Length.ofSI(1e-3);
48  
49      /** Typical lane width for which the typical lane change duration applies. */
50      private static final Length LANE_WIDTH = Length.ofSI(3.5);
51  
52      /** Max angle of numerical simplification. */
53      private static final double FLATTEN_ANGLE = Math.PI / 360.0;
54  
55      /** Flattener for paths. */
56      private static final Flattener2d FLATTENER = new MaxDeviationAndAngle(0.01, FLATTEN_ANGLE);
57  
58      /** Constructor. */
59      private LaneOperationalPlanBuilder()
60      {
61          // class should not be instantiated
62      }
63  
64      /**
65       * Builds a plan from any position not on a lane, towards a reasonable location on a lane on the route.
66       * @param gtu GTU
67       * @param acceleration acceleration
68       * @param duration time step
69       * @param tManeuver maneuver time, e.g. lane change
70       * @return plan from any position not on a lane, to a reasonable location on a lane on the route
71       * @throws IllegalStateException if the GTU is on a lane
72       */
73      public static LaneBasedOperationalPlan buildRoamingPlan(final LaneBasedGtu gtu, final Acceleration acceleration,
74              final Duration duration, final Duration tManeuver)
75      {
76          gtu.setLaneChangeDirection(LateralDirectionality.NONE);
77          LanePosition nearestPosition = gtu.getRoamingPosition();
78          Length deviation = Length.ZERO;
79          boolean deviative = true;
80          OtsLine2d path = getPath(gtu, nearestPosition, acceleration, duration, tManeuver, deviation, deviative);
81          Duration now = gtu.getSimulator().getSimulatorTime();
82          Segments segments = Segments.off(gtu.getSpeed(), duration, acceleration);
83          return Try.assign(() -> new LaneBasedOperationalPlan(gtu, path, now, segments, deviative),
84                  "Building roaming plan produced inconsistent LaneBasedOperationalPlan.");
85      }
86  
87      /**
88       * Build operational plan from a simple plan. Sets or resets the GTU lane change direction.
89       * @param gtu GTU
90       * @param simplePlan simple operational plan
91       * @param tManeuver maneuver time, e.g. lane change
92       * @return plan from position on a lane
93       * @throws IllegalStateException if the GTU is not on a lane
94       */
95      public static LaneBasedOperationalPlan buildPlanFromSimplePlan(final LaneBasedGtu gtu,
96              final SimpleOperationalPlan simplePlan, final Duration tManeuver)
97      {
98          Throw.when(gtu.getLane() == null, IllegalStateException.class,
99                  "Requested to build plan from simple plan for roaming GTU.");
100         boolean deviative = false;
101         LanePosition nearestPosition;
102         if (simplePlan.isLaneChange())
103         {
104             boolean start = gtu.getBookkeeping().equals(LaneBookkeeping.START)
105                     || (gtu.getBookkeeping().equals(LaneBookkeeping.START_AND_EDGE)
106                             && gtu.getSpeed().lt(LaneBookkeeping.START_THRESHOLD));
107             if (gtu.getBookkeeping().equals(LaneBookkeeping.INSTANT) || start)
108             {
109                 // changes the bookkeeping only, not the position
110                 gtu.changeLaneInstantaneously(simplePlan.getLaneChangeDirection());
111                 nearestPosition = gtu.getPosition();
112                 // only for INSTANT deviative should be false to make a jump to the center line
113                 deviative = start;
114             }
115             else
116             {
117                 gtu.setLaneChangeDirection(simplePlan.getLaneChangeDirection());
118                 deviative = true;
119                 Lane lane = gtu.getPosition().lane().getAdjacentLane(simplePlan.getLaneChangeDirection(), gtu.getType())
120                         .orElseThrow(() -> new IllegalStateException("Starting lane change without adjacent lane."));
121                 double fraction = lane.getCenterLine().projectFractional(lane.getLink().getStartNode().getHeading(),
122                         lane.getLink().getEndNode().getHeading(), gtu.getLocation().x, gtu.getLocation().y,
123                         FractionalFallback.ENDPOINT);
124                 nearestPosition = new LanePosition(lane, lane.getLength().times(fraction));
125             }
126         }
127         else
128         {
129             gtu.setLaneChangeDirection(LateralDirectionality.NONE);
130             nearestPosition = gtu.getPosition();
131             deviative = deviative || nearestPosition.getLocation().distance(gtu.getLocation()) > SNAP.si;
132         }
133         deviative = deviative || simplePlan.getDeviation().si > SNAP.si;
134         OtsLine2d path = getPath(gtu, nearestPosition, simplePlan.getAcceleration(), simplePlan.getDuration(), tManeuver,
135                 simplePlan.getDeviation(), deviative);
136         Duration now = gtu.getSimulator().getSimulatorTime();
137         Segments segments = Segments.off(gtu.getSpeed(), simplePlan.getDuration(), simplePlan.getAcceleration());
138         boolean finalDeviative = deviative;
139         return Try.assign(() -> new LaneBasedOperationalPlan(gtu, path, now, segments, finalDeviative),
140                 "Building operational plan produced inconsistent LaneBasedOperationalPlan.");
141     }
142 
143     /**
144      * Returns a path towards a target point that is found by moving along the lanes from the nearest position.
145      * @param gtu GTU
146      * @param nearestPosition nearest position, i.e. the the location of the GTU projected to the (target) lane, or some closest
147      *            point during roaming
148      * @param acceleration acceleration
149      * @param timeStep time step
150      * @param tManeuver maneuver time, e.g. lane change
151      * @param deviation desired deviation from lane center
152      * @param deviative true if the GTU will not strictly follow the center line
153      * @return path towards a target point that is found by moving along the lanes from the start position
154      */
155     private static OtsLine2d getPath(final LaneBasedGtu gtu, final LanePosition nearestPosition,
156             final Acceleration acceleration, final Duration timeStep, final Duration tManeuver, final Length deviation,
157             final boolean deviative)
158     {
159         Length laneGap = Length.ZERO;
160         HorizonSpace horizonSpace =
161                 getHorizonSpace(gtu, nearestPosition, acceleration, timeStep, tManeuver, deviation, laneGap);
162         if (deviative)
163         {
164             return bezierToHorizon(gtu, nearestPosition, deviation, horizonSpace);
165         }
166         return getCenterLinePath(gtu, nearestPosition, horizonSpace.horizon(), acceleration, timeStep, tManeuver, deviation);
167     }
168 
169     /**
170      * Create path as a Bezier to a target point that will be on the horizon.
171      * @param gtu GTU
172      * @param nearestPosition nearest position, i.e. the the location of the GTU projected to the (target) lane, or some closest
173      *            point during roaming
174      * @param deviation desired deviation from lane center
175      * @param horizonSpace information regarding the considered horizon
176      * @return path as a Bezier to a target point that will be on the horizon
177      */
178     private static OtsLine2d bezierToHorizon(final LaneBasedGtu gtu, final LanePosition nearestPosition, final Length deviation,
179             final HorizonSpace horizonSpace)
180     {
181         DirectedPoint2d target = getTargetPoint(gtu, nearestPosition, deviation, horizonSpace);
182         target = extrapolateToHorizon(gtu, target, horizonSpace.horizon());
183         return bezierToTarget(gtu, target);
184     }
185 
186     /**
187      * Returns a path constructed from lane center lines. If a gap between lanes is found, this method will revert back to a
188      * deviative path. The horizon is then recalculated including the found lane gap. A path to a target point on the horizon is
189      * then returned.
190      * @param gtu GTU
191      * @param nearestPosition start position
192      * @param length minimum
193      * @param acceleration acceleration
194      * @param timeStep time step
195      * @param tManeuver maneuver time, e.g. lane change
196      * @param deviation desired deviation from lane center
197      * @return path constructed from lane center lines, or a bezier path in case of a lane gap between center lines
198      */
199     private static OtsLine2d getCenterLinePath(final LaneBasedGtu gtu, final LanePosition nearestPosition, final Length length,
200             final Acceleration acceleration, final Duration timeStep, final Duration tManeuver, final Length deviation)
201     {
202         Length startDistance = nearestPosition.position();
203         Lane lane = nearestPosition.lane();
204         List<Point2d> points = new ArrayList<>();
205         Point2d lastPoint = null;
206         double cumulDist = 0.0;
207         while (lane != null)
208         {
209             OtsLine2d centerLine =
210                     startDistance.gt0() ? lane.getCenterLine().extract(startDistance, lane.getLength()) : lane.getCenterLine();
211             if (lastPoint != null && lastPoint.distance(centerLine.getFirst()) > SNAP.si)
212             {
213                 // It is not appropriate to follow the lane center lines due to a lane gap
214                 Length laneGap = Length.ofSI(lastPoint.distance(centerLine.getFirst()));
215                 HorizonSpace horizonSpace =
216                         getHorizonSpace(gtu, nearestPosition, acceleration, timeStep, tManeuver, deviation, laneGap);
217                 return bezierToHorizon(gtu, nearestPosition, deviation, horizonSpace);
218             }
219             for (Point2d point : centerLine)
220             {
221                 if (lastPoint != null)
222                 {
223                     double d = lastPoint.distance(point);
224                     if (d < SNAP.si)
225                     {
226                         continue;
227                     }
228                     cumulDist += d;
229                 }
230                 points.add(point);
231                 if (cumulDist >= length.si)
232                 {
233                     return new OtsLine2d(points);
234                 }
235                 lastPoint = point;
236             }
237             startDistance = Length.ZERO;
238             lane = gtu.getNextLaneForRoute(lane).orElse(null);
239         }
240         // Minimum length not reached, add extrapolated point to reach required length
241         Point2d lastLastPoint = points.get(points.size() - 2);
242         double direction = lastLastPoint.directionTo(lastPoint);
243         double r = length.si - cumulDist + SNAP.si;
244         points.add(lastPoint.translate(r * Math.cos(direction), r * Math.sin(direction)));
245         return new OtsLine2d(points);
246     }
247 
248     /**
249      * Returns relevant information regarding the considered horizon. The horizon (radius) is determined as:
250      * <ul>
251      * <li>At least the length of the operational plan</li>
252      * <li>At least the GTU turn radius (=diameter)</li>
253      * <li>At least several factors on tManeuver at the current speed:
254      * <ul>
255      * <li>[0...1] for a deviation from the desired deviation of [0...3.5]m, or 1 for larger deviation</li>
256      * <li>[0...1] for an angle of [0...pi/4] between the direction of the GTU and the direction at the target, or 1 for larger
257      * angles</li>
258      * <li>[2...0] for an angle of [0...pi/2] between the direction of the GTU and the direction towards the target, or 0 for
259      * larger angles</li>
260      * </ul>
261      * </li>
262      * </ul>
263      * The bullet on deviation implements a typical lane change horizon.<br>
264      * The before-last bullet reflects that maneuvers take more length when the GTU is at an angle.<br>
265      * The last bullet reflects roaming situations where the GTU is approaching the target at a near-right angle, in which case
266      * more rotation is required than a normal maneuver, and hence a factor larger than 1 is applied.
267      * @param gtu GTU
268      * @param nearestPosition nearest position, i.e. the location of the GTU projected to the (target) lane, or some closest
269      *            point during roaming
270      * @param acceleration acceleration
271      * @param timeStep time step
272      * @param tManeuver maneuver time, e.g. lane change
273      * @param deviation desired deviation from lane center
274      * @param laneGap known gap between longitudinally connected lanes
275      * @return information regarding the considered horizon
276      */
277     private static HorizonSpace getHorizonSpace(final LaneBasedGtu gtu, final LanePosition nearestPosition,
278             final Acceleration acceleration, final Duration timeStep, final Duration tManeuver, final Length deviation,
279             final Length laneGap)
280     {
281         DirectedPoint2d nearestPoint = nearestPosition.getLocation();
282         double dx = nearestPoint.x - gtu.getLocation().x;
283         double dy = nearestPoint.y - gtu.getLocation().y;
284         double dCenterLine = Math.hypot(dx, dy);
285         double leftOfLane;
286         if (dCenterLine <= SNAP.si)
287         {
288             leftOfLane = 1.0;
289         }
290         else if (dy == 0.0)
291         {
292             leftOfLane = -Math.signum(dx);
293         }
294         else if (dx == 0.0)
295         {
296             leftOfLane = -Math.signum(dy);
297         }
298         else
299         {
300             leftOfLane = Math.signum(Math.cos(Math.sin(nearestPoint.dirZ) * dx - nearestPoint.dirZ) * dy);
301         }
302         Length distanceToNearest = Length.ofSI(Math.abs(deviation.si - leftOfLane * dCenterLine));
303 
304         // Lateral deviation: 0 to 1 within first {LANE_WIDTH}m (also applies to lane gap)
305         double fLatDeviation = Math.min(1.0, Math.max(distanceToNearest.si, laneGap.si) / LANE_WIDTH.si);
306 
307         // Difference direction at target point and vehicle direction: 0 to 1 within pi/4
308         double dDirection = Math.abs(AngleUtil.normalizeAroundZero(nearestPoint.dirZ - gtu.getLocation().dirZ));
309         double fDirection = Math.min(1, 4 * dDirection / Math.PI);
310 
311         // Difference direction to target and vehicle direction: 2 to 0 within pi/2
312         // For these maneuvers more time is required than a normal lane change
313         Direction dirToNearest = Direction.ofSI(Math.atan2(dy, dx));
314         double fToTarget = 0.0;
315         if (dCenterLine > SNAP.si)
316         {
317             double dToTarget = Math.abs(AngleUtil.normalizeAroundZero(dirToNearest.si - gtu.getLocation().dirZ));
318             fToTarget = Math.max(0, 2 - 4 * dToTarget / Math.PI);
319         }
320 
321         // Combine factors
322         double tHorizon = tManeuver.si * Math.max(Math.max(fLatDeviation, fDirection), fToTarget);
323 
324         // Figure out horizon
325         Length turnDiameter = gtu.getVehicleModel().getTurnRadius(gtu);
326         double rPlan;
327         if (acceleration.lt0() && gtu.getSpeed().si / -acceleration.si < timeStep.si)
328         {
329             double t = gtu.getSpeed().si / -acceleration.si;
330             rPlan = gtu.getSpeed().si * t + .5 * acceleration.si * t * t;
331         }
332         else
333         {
334             rPlan = gtu.getSpeed().si * timeStep.si + .5 * acceleration.si * timeStep.si * timeStep.si;
335         }
336         Length horizon = Length.ofSI(Math.max(Math.max(turnDiameter.si, rPlan), gtu.getSpeed().si * tHorizon));
337         return new HorizonSpace(distanceToNearest, dirToNearest, horizon, turnDiameter, nearestPoint);
338     }
339 
340     /**
341      * Hold information on the horizon.
342      * @param distanceToNearest distance towards the nearest point
343      * @param dirToNearest distance to the nearest point
344      * @param horizon horizon radius
345      * @param turnDiameter GTU turn radius (=diameter)
346      * @param nearestPoint nearest point, e.g. GTU position projected to (target) lane
347      */
348     private record HorizonSpace(Length distanceToNearest, Direction dirToNearest, Length horizon, Length turnDiameter,
349             DirectedPoint2d nearestPoint)
350     {
351     }
352 
353     /**
354      * Returns a target point that is found by moving along the lanes from the nearest position. The general procedure is:
355      * <ul>
356      * <li>Far (closest point on lane is beyond horizon)</li>
357      * <ul>
358      * <li>Ahead (closest point on lane is within pi/4 of straight ahead)</li>
359      * <ul>
360      * <li>go to closest point on lane</li>
361      * </ul>
362      * <li>Behind</li>
363      * <ul>
364      * <li>turn to horizon edge closest to closest point on lane</li>
365      * </ul>
366      * </ul>
367      * <li>Close</li>
368      * <ul>
369      * <li>Intersect (horizon intersects lane within pi/4* of straight ahead)</li>
370      * <ul>
371      * <li>go to point where horizon intersects lane</li>
372      * </ul>
373      * <li>Prevent turn flipping (horizon edges closest to same point on lane)</li>
374      * <ul>
375      * <li>go to point on lane closest to either horizon edge</li>
376      * </ul>
377      * <li>Turn</li>
378      * <ul>
379      * <li>turn to horizon edge closest to lane</li>
380      * </ul>
381      * </ul>
382      * </ul>
383      * *) or narrower when limited by vehicle turning radius
384      * @param gtu GTU
385      * @param nearestPosition nearest position, i.e. the the location of the GTU projected to the (target) lane, or some closest
386      *            point during roaming
387      * @param deviation desired deviation from lane center
388      * @param horizonSpace information on horizon
389      * @return target point that is found by moving along the lanes from the nearest position
390      */
391     private static DirectedPoint2d getTargetPoint(final LaneBasedGtu gtu, final LanePosition nearestPosition,
392             final Length deviation, final HorizonSpace horizonSpace)
393     {
394         if (horizonSpace.distanceToNearest().gt(horizonSpace.horizon()))
395         {
396             // Far away: lane is beyond horizon, go to closest point
397             if (Math.abs(horizonSpace.dirToNearest().si - gtu.getLocation().dirZ) < Math.PI / 4.0)
398             {
399                 // Closest point is ahead, use direction but limit to horizon for reasonable path curvature
400                 return new DirectedPoint2d(
401                         gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(horizonSpace.dirToNearest().si),
402                         gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(horizonSpace.dirToNearest().si),
403                         horizonSpace.dirToNearest().si);
404             }
405             else
406             {
407                 // Closest point is behind, go to edge of the horizon (left or right) that is closest to closest point on lane
408                 double alpha = Math.PI / 2.0;
409                 double alphaMin = gtu.getLocation().dirZ - alpha;
410                 double alphaMax = gtu.getLocation().dirZ + alpha;
411 
412                 double x1 = gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(alphaMin);
413                 double y1 = gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(alphaMin);
414                 Point2d nearest1 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(new Point2d(x1, y1));
415                 double dist1sq = Math.hypot(x1 - nearest1.x, y1 - nearest1.y);
416 
417                 double x2 = gtu.getLocation().x + horizonSpace.horizon().si * Math.cos(alphaMax);
418                 double y2 = gtu.getLocation().y + horizonSpace.horizon().si * Math.sin(alphaMax);
419                 Point2d nearest2 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(new Point2d(x2, y2));
420                 double dist2sq = Math.hypot(x2 - nearest2.x, y2 - nearest2.y);
421 
422                 if (nearest1.directionTo(nearest2) < SNAP.si)
423                 {
424                     // Same point, let's not flip left/right each step, but just go there
425                     double dirToAdjustedTarget = Math.atan2(nearest1.y - gtu.getLocation().y, nearest1.x - gtu.getLocation().x);
426                     return new DirectedPoint2d(nearest1, dirToAdjustedTarget);
427                 }
428                 if (dist1sq <= dist2sq)
429                 {
430                     return new DirectedPoint2d(x1, y1, gtu.getLocation().dirZ + Math.PI);
431                 }
432                 return new DirectedPoint2d(x2, y2, gtu.getLocation().dirZ - Math.PI);
433             }
434         }
435 
436         // Close(ish): go to point where horizon intersects lane
437         double alpha = Math.PI / 4.0;
438         double rVehicle = horizonSpace.turnDiameter().si;
439         double rHorizon = horizonSpace.horizon().si;
440         if (rVehicle > .5 * rHorizon)
441         {
442             // Not the whole horizon might be reachable due to turn radius
443 
444             /* {@formatter:off}
445              * Intersection of 2 circles to find max horizon angle (alpha|a)
446              *  1) circle with radius rHorizon around (0, 0) = A
447              *  2) circle with radius rVehicle around (-rVehicle, 0) = B
448              * Intersection P creates triangle with circle centers A and B.
449              * Arc A-P is how the vehicle can turn towards horizon at P.
450              * Side A-P of length rHorizon is split at mid-point "#".
451              * A new line B-# creates two right triangles /\#-B-P & /\#-B-A.
452              * Line B-# has length "z". To find alpha:
453              *  - Note that /_P-A-B is 90deg - a. Angle /_#-B-A is 90deg
454              *    minus /_P-A-B, and therefore /_#-B-A = a.
455              *  - From this: a = arctan(.5 * rHorizon / z) =>
456              *    z = .5 * rHorizon / tan(a)
457              *  - Pythagoras gives: z = sqrt(rVehicle^2 - (.5 * rHorizon)^2)
458              *  - Equating and solving for a, simplifying fraction in tan by
459              *    multiplying numerator and denominator by 2=sqrt(4), gives:
460              *
461              *     a = arctan(rHorizon / sqrt(4 * rVehicle^2 - rHorizon^2))
462              *
463              *            T    |   | <--"ahead" in plane of vehicle
464              *             '.2a|   |
465              *               '.P-''|''-..
466              *    rVehicle .-'  \  |rHorizon
467              *          .-'/ rHor#a|       \
468              *       .-'  /       \|        \ <--horizon at rHorizon
469              *     B---------------A         |
470              *       ^  rVehicle    (0, 0)  /
471              *       |     \               /
472              *  turn radius '.           .'
473              *                '-..___..-'
474              *
475              * {@formatter:on}
476              */
477             alpha = Math.min(alpha, Math.atan(rHorizon / Math.sqrt(4 * rVehicle * rVehicle - rHorizon * rHorizon)));
478         }
479 
480         // Return intersection of lane path and horizon
481         LanePosition endPosition = getTargetLanePosition(gtu, nearestPosition, horizonSpace.horizon(), Angle.ofSI(alpha));
482         if (endPosition != null)
483         {
484             return endPosition.getLocation();
485         }
486 
487         // Make turn
488         double alphaMin = gtu.getLocation().dirZ - alpha;
489         double alphaMax = gtu.getLocation().dirZ + alpha;
490         Point2d pMin = new Point2d(gtu.getLocation().x + rHorizon * Math.cos(alphaMin),
491                 gtu.getLocation().y + rHorizon * Math.sin(alphaMin));
492         Point2d nearest1 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(pMin);
493         double dist1sq = nearest1.distance(pMin);
494         Point2d pMax = new Point2d(gtu.getLocation().x + rHorizon * Math.cos(alphaMax),
495                 gtu.getLocation().y + rHorizon * Math.sin(alphaMax));
496         Point2d nearest2 = nearestPosition.lane().getCenterLine().closestPointOnPolyLine(pMax);
497         double dist2sq = nearest2.distance(pMax);
498         if (nearest1.distance(nearest2) < SNAP.si)
499         {
500             // Same point, let's not flip left/right every step, but just go there
501             return new DirectedPoint2d(nearest1, gtu.getLocation().directionTo(nearest1));
502         }
503         else if (dist1sq <= dist2sq)
504         {
505             /*
506              * 2 * alpha is added or subtracted. This is to obtain the direction of line line P-T in the figure above, which is
507              * the same as the angle of the arc A-P at P. The arc hits P at an angle 'a' with the line A-P (symmetry). The line
508              * A-P is also at an angle 'a' relative to vertical. Hence, line P-T makes an angle of 2a as this is the opposite
509              * corner between vertical and P-T.
510              */
511             return new DirectedPoint2d(nearest1, gtu.getLocation().dirZ - 2 * alpha);
512         }
513         return new DirectedPoint2d(nearest2, gtu.getLocation().dirZ + 2 * alpha);
514     }
515 
516     /**
517      * Returns the target position by following lanes from the start position until a point is found that is at the horizon
518      * distance removed from the GTU. If such a point is not within the viewport, {@code null} is returned. This method is part
519      * of {@code getTargetPoint()}.
520      * @param gtu GTU
521      * @param startPosition start position of path, which should be the projected location on an adjacent lane for a lane change
522      * @param horizon distance as the crow flies of the next target point
523      * @param viewport horizontal angle within which the horizon is considered, relative to straight ahead, both left and right
524      * @return lane path for a movement step of a GTU, {@code null} if no such point within viewport
525      */
526     private static LanePosition getTargetLanePosition(final LaneBasedGtu gtu, final LanePosition startPosition,
527             final Length horizon, final Angle viewport)
528     {
529         DirectedPoint2d loc0 = gtu.getLocation();
530         OtsLine2d laneCenter =
531                 startPosition.lane().getCenterLine().extract(startPosition.position(), startPosition.lane().getLength());
532         Lane lane = startPosition.lane();
533         Set<Lane> coveredLanes = new LinkedHashSet<>();
534         double r2 = horizon.si * horizon.si;
535         double distCumulLane = startPosition.position().si;
536         while (!coveredLanes.contains(lane))
537         {
538             coveredLanes.add(lane);
539 
540             // If the first point on the next lane is beyond the horizon, the horizon is in a gap between two lanes. The first
541             // point of the next lane can be considered the focus point.
542             if (Math.hypot(laneCenter.getFirst().x - loc0.x, laneCenter.getFirst().y - loc0.y) > horizon.si)
543             {
544                 double alpha = Math.atan2(laneCenter.getFirst().y - loc0.y, laneCenter.getFirst().x - loc0.x) - loc0.dirZ;
545                 return Math.abs(alpha) <= viewport.si ? new LanePosition(lane, Length.ZERO) : null;
546             }
547 
548             // Find horizon point on lane
549             for (int i = 0; i < laneCenter.size() - 1; i++)
550             {
551                 double dx = laneCenter.getX(i + 1) - laneCenter.getX(i);
552                 double dy = laneCenter.getY(i + 1) - laneCenter.getY(i);
553                 double dr = Math.hypot(dx, dy);
554 
555                 // Check if line segment crosses circle (method from https://mathworld.wolfram.com/Circle-LineIntersection.html)
556                 double det = (laneCenter.getX(i) - loc0.x) * (laneCenter.getY(i + 1) - loc0.y)
557                         - (laneCenter.getX(i + 1) - loc0.x) * (laneCenter.getY(i) - loc0.y);
558                 double dr2 = dr * dr;
559                 double det2 = det * det;
560                 double discriminant = r2 * dr2 - det2;
561                 if (discriminant >= 0.0)
562                 {
563                     double sgn = dy < 0.0 ? -1.0 : 1.0;
564                     double sqrtDisc = Math.sqrt(discriminant);
565                     // Up to two crossing points
566                     double pointSign = 1.0;
567                     for (int j = 0; j < 2; j++)
568                     {
569                         double xP = loc0.x + (det * dy + pointSign * sgn * dx * sqrtDisc) / dr2;
570                         double yP = loc0.y + (-det * dx + pointSign * Math.abs(dy) * sqrtDisc) / dr2;
571                         double alpha = AngleUtil.normalizeAroundZero(Math.atan2(yP - loc0.y, xP - loc0.x) - loc0.dirZ);
572 
573                         double f = Math.abs(dx) > 0.0 && Math.abs(dx) > Math.abs(dy) ? (xP - laneCenter.getX(i)) / dx
574                                 : (dy == 0.0 ? 0.0 : (yP - laneCenter.getY(i)) / dy);
575                         // Crosses within line segment?
576                         if (f >= 0.0 && f <= 1.0)
577                         {
578                             // In viewing port?
579                             if (Math.abs(alpha) <= viewport.si)
580                             {
581                                 return new LanePosition(lane, Length.ofSI(distCumulLane + f * dr));
582                             }
583                             else
584                             {
585                                 // Crossing with center line outside of viewport (could be turn radius). Increase horizon and
586                                 // try again but with full default viewport.
587                                 return getTargetLanePosition(gtu, startPosition, horizon.times(2.0), Angle.ofSI(Math.PI / 4.0));
588                             }
589                         }
590                         pointSign = -1.0;
591                     }
592                 }
593                 distCumulLane += dr;
594             }
595 
596             // Move to next lane
597             Optional<Lane> nextLane = gtu.getNextLaneForRoute(lane);
598             if (nextLane.isEmpty())
599             {
600                 return new LanePosition(lane, Length.ofSI(startPosition.lane().getCenterLine().getLength()));
601             }
602             lane = nextLane.get();
603             laneCenter = lane.getCenterLine();
604             distCumulLane = 0.0;
605         }
606 
607         // Encountered a loop, return last position if within alpha
608         double alpha = AngleUtil
609                 .normalizeAroundZero(Math.atan2(laneCenter.getLast().y - loc0.y, laneCenter.getLast().x - loc0.x) - loc0.dirZ);
610         return Math.abs(alpha) <= viewport.si ? new LanePosition(lane, lane.getLength()) : null;
611     }
612 
613     /**
614      * Extrapolate target point further in its direction up to horizon, if it is closer than horizon. This can happen if the end
615      * of a route is reached.
616      * @param gtu GTU
617      * @param target target point
618      * @param horizon horizon
619      * @return extrapolated target point further in its direction up to horizon
620      */
621     private static DirectedPoint2d extrapolateToHorizon(final LaneBasedGtu gtu, final DirectedPoint2d target,
622             final Length horizon)
623     {
624         double dx = target.x - gtu.getLocation().x;
625         double dy = target.y - gtu.getLocation().y;
626         double dist = Math.hypot(dx, dy);
627         if (dist >= horizon.si)
628         {
629             return target;
630         }
631         /*
632          * {@formatter:off}
633          * Relative to vehicle (A), we need a point P on the horizon at an
634          * angle 'a'. This point is 'h' extrapolated beyond target (B) at
635          * (dx, dy) at angle target.dirZ.
636          *  x = dx + h * cos(target.dirZ) = horizon * cos(a)       [1]
637          *  y = dy + h * sin(target.dirZ) = horizon * sin(a)       [2]
638          * Solving a = f(...) for [2] and substituting in [1], and
639          * solving this for h, gives a large equation with two solutions.
640          *
641          *                 ..--''' <-- Horizon at horizon from A
642          * target.dirZ  .-'h
643          *     <-------P------B (dx, dy) <-- target
644          *      (x, y)' ''-.  a\
645          *           |      ''-.A <-- vehicle
646          *            .          (0, 0)
647          * {@formatter:on}
648          */
649         double cosTarget = Math.cos(target.dirZ);
650         double sinTarget = Math.sin(target.dirZ);
651         double c = cosTarget * dx;
652         double s = sinTarget * dy;
653         double d = Math.sqrt(
654                 -cosTarget * cosTarget * dy * dy + 2.0 * c * s - sinTarget * sinTarget * dx * dx + horizon.si * horizon.si);
655         double x = Math.max(-c - s - d, -c - s + d); // positive solution
656         return new DirectedPoint2d(target.x + x * cosTarget, target.y + x * sinTarget, target.dirZ);
657     }
658 
659     /**
660      * Create Bezier path from current location of the GTU towards the target point. The path is flattened with a default
661      * flattener using a maximum deviation of 0.1m and maximum angle 0.5 degrees.
662      * @param gtu GTU
663      * @param target target point
664      * @return Bezier path from current location of the GTU towards the target point
665      */
666     private static OtsLine2d bezierToTarget(final LaneBasedGtu gtu, final DirectedPoint2d target)
667     {
668         double angleShift = Math.abs(AngleUtil.normalizeAroundZero(gtu.getLocation().dirZ - target.dirZ));
669         double dirToTarget = gtu.getLocation().directionTo(target);
670         if (angleShift < FLATTEN_ANGLE && Math.abs(AngleUtil.normalizeAroundZero(dirToTarget - target.dirZ)) < FLATTEN_ANGLE
671                 && Math.abs(AngleUtil.normalizeAroundZero(dirToTarget - gtu.getLocation().dirZ)) < FLATTEN_ANGLE)
672         {
673             // current position and direction sufficiently in line with target position and direction to simplify as straight
674             return new OtsLine2d(gtu.getLocation(), target);
675         }
676         // Shape points at shapeFactor of inter-point distance:
677         // 1/3rd when angle between points < pi/2
678         // then increases linearly to 2/3rds for an angle of pi
679         double shapeFactor = (1.0 + 2.0 * Math.max(0.0, angleShift - 0.5 * Math.PI) / Math.PI) / 3;
680         double rControl = shapeFactor * Math.hypot(gtu.getLocation().x - target.x, gtu.getLocation().y - target.y);
681         Point2d p2 = new Point2d(gtu.getLocation().x + rControl * Math.cos(gtu.getLocation().dirZ),
682                 gtu.getLocation().y + rControl * Math.sin(gtu.getLocation().dirZ));
683         Point2d p3 = new Point2d(target.x - rControl * Math.cos(target.dirZ), target.y - rControl * Math.sin(target.dirZ));
684         BezierCubic2d bezier = new BezierCubic2d(gtu.getLocation(), p2, p3, target);
685         return new OtsLine2d(bezier.toPolyLine(FLATTENER));
686     }
687 
688 }