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
33
34
35
36
37
38
39
40
41
42
43 public final class LaneOperationalPlanBuilder
44 {
45
46
47 private static final Length SNAP = Length.ofSI(1e-3);
48
49
50 private static final Length LANE_WIDTH = Length.ofSI(3.5);
51
52
53 private static final double FLATTEN_ANGLE = Math.PI / 360.0;
54
55
56 private static final Flattener2d FLATTENER = new MaxDeviationAndAngle(0.01, FLATTEN_ANGLE);
57
58
59 private LaneOperationalPlanBuilder()
60 {
61
62 }
63
64
65
66
67
68
69
70
71
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
89
90
91
92
93
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
110 gtu.changeLaneInstantaneously(simplePlan.getLaneChangeDirection());
111 nearestPosition = gtu.getPosition();
112
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
145
146
147
148
149
150
151
152
153
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
171
172
173
174
175
176
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
188
189
190
191
192
193
194
195
196
197
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
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
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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
305 double fLatDeviation = Math.min(1.0, Math.max(distanceToNearest.si, laneGap.si) / LANE_WIDTH.si);
306
307
308 double dDirection = Math.abs(AngleUtil.normalizeAroundZero(nearestPoint.dirZ - gtu.getLocation().dirZ));
309 double fDirection = Math.min(1, 4 * dDirection / Math.PI);
310
311
312
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
322 double tHorizon = tManeuver.si * Math.max(Math.max(fLatDeviation, fDirection), fToTarget);
323
324
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
342
343
344
345
346
347
348 private record HorizonSpace(Length distanceToNearest, Direction dirToNearest, Length horizon, Length turnDiameter,
349 DirectedPoint2d nearestPoint)
350 {
351 }
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391 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
397 if (Math.abs(horizonSpace.dirToNearest().si - gtu.getLocation().dirZ) < Math.PI / 4.0)
398 {
399
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
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
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
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
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477 alpha = Math.min(alpha, Math.atan(rHorizon / Math.sqrt(4 * rVehicle * rVehicle - rHorizon * rHorizon)));
478 }
479
480
481 LanePosition endPosition = getTargetLanePosition(gtu, nearestPosition, horizonSpace.horizon(), Angle.ofSI(alpha));
482 if (endPosition != null)
483 {
484 return endPosition.getLocation();
485 }
486
487
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
501 return new DirectedPoint2d(nearest1, gtu.getLocation().directionTo(nearest1));
502 }
503 else if (dist1sq <= dist2sq)
504 {
505
506
507
508
509
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
518
519
520
521
522
523
524
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
541
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
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
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
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
576 if (f >= 0.0 && f <= 1.0)
577 {
578
579 if (Math.abs(alpha) <= viewport.si)
580 {
581 return new LanePosition(lane, Length.ofSI(distCumulLane + f * dr));
582 }
583 else
584 {
585
586
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
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
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
615
616
617
618
619
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
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
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);
656 return new DirectedPoint2d(target.x + x * cosTarget, target.y + x * sinTarget, target.dirZ);
657 }
658
659
660
661
662
663
664
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
674 return new OtsLine2d(gtu.getLocation(), target);
675 }
676
677
678
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 }