View Javadoc
1   package org.opentrafficsim.road.gtu.tactical.util;
2   
3   import java.util.ArrayList;
4   import java.util.Iterator;
5   import java.util.LinkedHashMap;
6   import java.util.LinkedHashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Optional;
10  import java.util.Set;
11  import java.util.UUID;
12  import java.util.function.Function;
13  import java.util.function.Supplier;
14  
15  import org.djunits.unit.AccelerationUnit;
16  import org.djunits.unit.DurationUnit;
17  import org.djunits.unit.LengthUnit;
18  import org.djunits.value.vdouble.scalar.Acceleration;
19  import org.djunits.value.vdouble.scalar.Duration;
20  import org.djunits.value.vdouble.scalar.Length;
21  import org.djunits.value.vdouble.scalar.Speed;
22  import org.djunits.value.vdouble.scalar.Time;
23  import org.djutils.exceptions.Throw;
24  import org.opentrafficsim.base.OtsRuntimeException;
25  import org.opentrafficsim.base.logger.Logger;
26  import org.opentrafficsim.base.parameters.ParameterException;
27  import org.opentrafficsim.base.parameters.ParameterTypeAcceleration;
28  import org.opentrafficsim.base.parameters.ParameterTypeBoolean;
29  import org.opentrafficsim.base.parameters.ParameterTypeDouble;
30  import org.opentrafficsim.base.parameters.ParameterTypeDuration;
31  import org.opentrafficsim.base.parameters.ParameterTypeLength;
32  import org.opentrafficsim.base.parameters.ParameterTypes;
33  import org.opentrafficsim.base.parameters.Parameters;
34  import org.opentrafficsim.base.parameters.constraint.ConstraintInterface;
35  import org.opentrafficsim.core.definitions.DefaultsNl;
36  import org.opentrafficsim.core.gtu.GtuException;
37  import org.opentrafficsim.core.gtu.TurnIndicatorStatus;
38  import org.opentrafficsim.core.network.Node;
39  import org.opentrafficsim.core.network.route.Route;
40  import org.opentrafficsim.road.gtu.LaneBasedGtu;
41  import org.opentrafficsim.road.gtu.perception.PerceptionCollectable;
42  import org.opentrafficsim.road.gtu.perception.PerceptionCollectable.PerceptionAccumulator;
43  import org.opentrafficsim.road.gtu.perception.PerceptionCollectable.PerceptionCollector;
44  import org.opentrafficsim.road.gtu.perception.PerceptionIterable;
45  import org.opentrafficsim.road.gtu.perception.RelativeLane;
46  import org.opentrafficsim.road.gtu.perception.categories.IntersectionPerception;
47  import org.opentrafficsim.road.gtu.perception.categories.neighbors.NeighborsPerception;
48  import org.opentrafficsim.road.gtu.perception.object.PerceivedConflict;
49  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtu;
50  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtu.Maneuver;
51  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtu.Signals;
52  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtuBase;
53  import org.opentrafficsim.road.gtu.perception.object.PerceivedGtuSimple;
54  import org.opentrafficsim.road.gtu.perception.object.PerceivedObject;
55  import org.opentrafficsim.road.gtu.perception.object.PerceivedObject.Kinematics;
56  import org.opentrafficsim.road.gtu.tactical.Blockable;
57  import org.opentrafficsim.road.gtu.tactical.TacticalContext;
58  import org.opentrafficsim.road.gtu.tactical.TacticalContextEgo;
59  import org.opentrafficsim.road.gtu.tactical.lmrs.AccelerationIncentive;
60  import org.opentrafficsim.road.gtu.tactical.pt.BusSchedule;
61  import org.opentrafficsim.road.network.CrossSectionLink;
62  import org.opentrafficsim.road.network.conflict.BusStopConflictRule;
63  import org.opentrafficsim.road.network.conflict.ConflictRule;
64  
65  /**
66   * This class implements default behavior for intersection conflicts for use in tactical planners.
67   * <p>
68   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
69   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
70   * </p>
71   * @author Alexander Verbraeck
72   * @author Peter Knoppers
73   * @author Wouter Schakel
74   * @see <a href="https://rstrail.nl/wp-content/uploads/2015/02/schakel_2012.pdf">Schakel, W.J., B. van Arem (2012) “An Urban
75   *      Traffic Extension of a Freeway Driver Model for use in the OpenTraffic® Open Source Traffic Simulation”, presented at
76   *      TRAIL Congress 2012.</a>
77   */
78  // TODO do not ignore vehicles upstream of conflict if they have green
79  // TODO conflict over multiple lanes (longitudinal in own direction)
80  // TODO a) yielding while having priority happens only when leaders is standing still on conflict (then its useless...)
81  // b) two vehicles can remain upstream of merge if vehicle stands on merge but leaves some space to move
82  // probably 1 is yielding, and 1 is courtesy yielding as the other stands still
83  // c) they might start moving together and collide further down (no response to negative headway on merge)
84  public final class ConflictUtil
85  {
86  
87      /** Minimum time gap between events. */
88      public static final ParameterTypeDuration MIN_GAP = new ParameterTypeDuration("minGap", "Minimum gap for conflicts",
89              new Duration(0.000001, DurationUnit.SECOND), ConstraintInterface.POSITIVE);
90  
91      /** Comfortable deceleration. */
92      public static final ParameterTypeAcceleration B = ParameterTypes.B;
93  
94      /** Critical deceleration. */
95      public static final ParameterTypeAcceleration BCRIT = ParameterTypes.BCRIT;
96  
97      /** Stopping distance. */
98      public static final ParameterTypeLength S0 = ParameterTypes.S0;
99  
100     /** Stopping distance at conflicts. */
101     public static final ParameterTypeLength S0_CONF = new ParameterTypeLength("s0conf", "Stopping distance at conflicts",
102             new Length(1.5, LengthUnit.METER), ConstraintInterface.POSITIVE);
103 
104     /** Multiplication factor on time for conservative assessment. */
105     public static final ParameterTypeDouble TIME_FACTOR =
106             new ParameterTypeDouble("timeFactor", "Safety factor on estimated time", 1.25, ConstraintInterface.ATLEASTONE);
107 
108     /** Area before stop line where one is considered arrived at the intersection. */
109     public static final ParameterTypeLength STOP_AREA =
110             new ParameterTypeLength("stopArea", "Area before stop line where one is considered arrived at the intersection",
111                     new Length(4, LengthUnit.METER), ConstraintInterface.POSITIVE);
112 
113     /** Parameter of how much time before departure a bus indicates its departure to get priority. */
114     public static final ParameterTypeDuration TI = new ParameterTypeDuration("ti", "Indicator time before bus departure",
115             Duration.ofSI(3.0), ConstraintInterface.POSITIVE);
116 
117     /** Parameter to deviate laterally at splits. */
118     public static final ParameterTypeBoolean DEV_SPLIT =
119             new ParameterTypeBoolean("dev_split", "Deviate laterally at splits.", false);
120 
121     /** Time step for free acceleration anticipation. */
122     private static final Duration TIME_STEP = Duration.ofSI(0.5);
123 
124     /** Cross standing vehicles on crossings. We allow this to prevent dead-locks. A better model should render this useless. */
125     private static final boolean CROSSSTANDING = true;
126 
127     /** Registry of crossing events as one GTU, is crossing some stand-still other GTU. */
128     private static final ThreadLocal<Map<String, Set<String>>> CROSSEVENTS =
129             ThreadLocal.withInitial(() -> new LinkedHashMap<>());
130 
131     /**
132      * Do not instantiate.
133      */
134     private ConflictUtil()
135     {
136         //
137     }
138 
139     /**
140      * Approach conflicts by applying appropriate acceleration (or deceleration). The model may yield for a vehicle even while
141      * having priority. Such a plan is remembered in {@link ConflictPlans}. By forwarding the same {@code ConflictPlans} for a
142      * GTU consistency of such plans is provided. If any conflict is not accepted to pass, stopping before a more upstream
143      * conflict is applied if there is not sufficient stopping length in between conflicts.
144      * @param context tactical information such as parameters and car-following model
145      * @param conflictPlans set of plans for conflict
146      * @param lane lane
147      * @param mergeDistance distance along which no lane changes can be performed towards the lane
148      * @param onRoute filter conflicts to only include conflict on the route
149      * @return acceleration appropriate for approaching the conflicts
150      * @throws GtuException in case of an unsupported conflict rule
151      * @throws ParameterException if a parameter is not defined or out of bounds
152      */
153     @SuppressWarnings("checkstyle:methodlength")
154     // @docs/06-behavior/tactical-planner/#modular-utilities (..., final ConflictPlans conflictPlans, ...)
155     public static Acceleration approachConflicts(final TacticalContextEgo context, final ConflictPlans conflictPlans,
156             final RelativeLane lane, final Length mergeDistance, final boolean onRoute) throws GtuException, ParameterException
157     {
158         Iterable<PerceivedConflict> conflicts =
159                 context.getPerception().getPerceptionCategory(IntersectionPerception.class).getConflicts(lane);
160         conflicts = AccelerationIncentive.onRoad(conflicts, lane, mergeDistance);
161         if (onRoute)
162         {
163             conflicts = AccelerationIncentive.onRoute(conflicts, context.getRoute().orElse(null));
164         }
165         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> leaders =
166                 context.getPerception().getPerceptionCategory(NeighborsPerception.class).getLeaders(lane);
167 
168         boolean blocking = false;
169 
170         // Ignore conflicts if we are beyond a stopping distance
171         Acceleration a = Acceleration.POS_MAXVALUE;
172         Length stoppingDistance = Length.ofSI(context.getParameters().getParameter(S0).si + context.getLength().si
173                 + .5 * context.getSpeed().si * context.getSpeed().si / context.getParameters().getParameter(B).si);
174         Iterator<PerceivedConflict> it = conflicts.iterator();
175         if (it.hasNext() && it.next().getDistance().gt(stoppingDistance))
176         {
177             conflictPlans.setBlocking(blocking);
178             return a;
179         }
180 
181         // Maintain lists of info per conflict required for consistency in a plan to cross (part of) an intersection
182         List<Length> prevStarts = new ArrayList<>();
183         List<Length> prevEnds = new ArrayList<>();
184         List<Class<? extends ConflictRule>> conflictRuleTypes = new ArrayList<>();
185 
186         // Distance until first stationary leader, minus spaces required for stationary intermediate vehicle and minus ego
187         Space space = leaders.collect(new AvailableSpace());
188         Length availableSpace = space.availableSpace().minus(passableDistance(context.getLength(), context.getParameters()));
189         /*
190          * We subtract any space that is likely not to be used by a queue ahead. This is the length of all crossing and merge
191          * conflicts between the first leader, and the first stand-still leader. If the rear of the first stand-still leader is
192          * on a conflict, only the length of that conflict up to the rear is subtracted. If the first non-split conflict is a
193          * merge, it's length is never subtracted. That would prevent taking priority when its given for zip-merging.
194          */
195         Length firstLeader = leaders.isEmpty() ? Length.POS_MAXVALUE : leaders.first().getDistance();
196         boolean first = true;
197         for (PerceivedConflict conflict : conflicts)
198         {
199             if (conflict.getDistance().gt(space.firstStationary()))
200             {
201                 break;
202             }
203 
204             if (!conflict.isSplit())
205             {
206                 Length conflictEnd = conflict.getDistance().plus(conflict.getLength());
207                 if ((!first || conflict.isCrossing()) && conflictEnd.gt(firstLeader))
208                 {
209                     Length effectiveEnd = Length.min(space.firstStationary(), conflictEnd);
210                     Length effectiveLength = effectiveEnd.minus(conflict.getDistance());
211                     availableSpace = availableSpace.minus(effectiveLength);
212                 }
213                 first = false;
214             }
215         }
216 
217         for (PerceivedConflict conflict : conflicts)
218         {
219             // adjust acceleration for situations where stopping might not be required
220             if (conflict.isCrossing())
221             {
222                 // avoid collision if crossing is occupied
223                 a = Acceleration.min(a, avoidCrossingCollision(context, conflict));
224             }
225             else
226             {
227                 if (conflict.isMerge() && !lane.isCurrent() && conflict.getConflictPriority().isPriority())
228                 {
229                     // this is probably evaluation for a lane-change as this it not on the current lane
230                     a = Acceleration.min(a, avoidMergeCollision(context, conflict));
231                 }
232 
233                 // lateral deviation intent on split
234                 lateralDeviationAtSplit(context, conflict, leaders, availableSpace);
235 
236                 // follow leading GTUs on merge or split
237                 a = Acceleration.min(a, followConflictingLeaderOnMergeOrSplit(context, conflict));
238             }
239 
240             // indicator if bus
241             if (lane.isCurrent())
242             {
243                 // TODO priority rules of busses should be handled differently
244                 // This also makes a GTU type and -Ego- context unnecessary here
245                 Optional<Route> route = context.getRoute();
246                 if (route.isPresent() && route.get() instanceof BusSchedule busSchedule
247                         && context.getGtuType().isOfType(DefaultsNl.BUS)
248                         && conflict.getConflictRuleType().equals(BusStopConflictRule.class))
249                 {
250                     Optional<Duration> actualDeparture = busSchedule.getActualDepartureConflict(conflict.getId());
251                     if (actualDeparture.isPresent()
252                             && actualDeparture.get().si < context.getTime().si + context.getParameters().getParameter(TI).si)
253                     {
254                         // TODO depending on left/right-hand traffic
255                         context.addIntent(TurnIndicatorStatus.LEFT, conflict.getDistance());
256                     }
257                 }
258             }
259 
260             // blocking and ignoring
261             if (conflict.getDistance().lt0() && lane.isCurrent())
262             {
263                 if (conflict.getConflictType().isCrossing() && !conflict.getConflictPriority().isPriority())
264                 {
265                     // note that we are blocking a conflict
266                     blocking = true;
267                 }
268                 // ignore conflicts we are on (i.e. negative distance to start of conflict)
269                 continue;
270             }
271 
272             // zip-merging
273             boolean stop = false;
274             if (conflict.isMerge() && conflict.getConflictPriority().isPriority())
275             {
276                 if (conflict.getUpstreamConflictingGtus().isEmpty()
277                         || !conflictPlans.isZipGtu(conflict.getUpstreamConflictingGtus().first().getId()))
278                 {
279                     conflictPlans.clearZipGtu();
280                 }
281                 else
282                 {
283                     stop = true;
284                 }
285             }
286 
287             // determine if we need to stop by available space downstream
288             if (!stop)
289             {
290                 Length d = conflict.isCrossing() ? conflict.getDistance().plus(conflict.getLength()) : conflict.getDistance();
291                 stop = !conflict.getConflictType().isSplit() && d.lt(space.firstStationary()) && availableSpace.lt(d);
292 
293                 // trigger zip-merging
294                 /*
295                  * Note that when a vehicle is fully on a merge, only vehicles from the same direction consider it regarding
296                  * available space. Vehicles from the other direction do not have the vehicle as one of their regular leaders.
297                  * One vehicle from the other direction will thus put the nose on the merge or close to it. If this is on the
298                  * merge, zip behavior automatically results. If it is close to the merge, and the vehicle on the conflict was
299                  * from the priority direction, the priority vehicle upstream of the conflict needs to remember to let the other
300                  * vehicle go. Otherwise, as soon as traffic starts to move and the available space heuristic is lifted, regular
301                  * priority behavior results. This may cause the non-priority direction to never flow.
302                  */
303                 if (stop && conflict.isMerge() && conflict.getConflictPriority().isPriority() && conflict.getDistance().gt0()
304                         && !conflict.getUpstreamConflictingGtus().isEmpty() && conflict.getUpstreamConflictingGtus().first()
305                                 .getDistance().lt(context.getParameters().getParameter(STOP_AREA)))
306                 {
307                     conflictPlans.setZipGtu(conflict.getUpstreamConflictingGtus().first().getId());
308                 }
309             }
310 
311             if (!stop)
312             {
313                 switch (conflict.getConflictPriority())
314                 {
315                     case PRIORITY:
316                     {
317                         // available space consideration and zip-merging allow no further action
318                         break;
319                     }
320                     case YIELD:
321                     {
322                         Length prevEnd = prevEnds.isEmpty() ? null : prevEnds.get(prevEnds.size() - 1);
323                         stop = stopForGiveWayConflict(context, conflict, leaders, blocking ? BCRIT : B, prevEnd);
324                         break;
325                     }
326                     case STOP:
327                     {
328                         Length prevEnd = prevEnds.isEmpty() ? null : prevEnds.get(prevEnds.size() - 1);
329                         stop = stopForStopConflict(context, conflict, leaders, blocking ? BCRIT : B, prevEnd);
330                         break;
331                     }
332                     case ALL_STOP:
333                     {
334                         stop = stopForAllStopConflict(conflict, conflictPlans);
335                         break;
336                     }
337                     case SPLIT:
338                     {
339                         continue;
340                     }
341                     default:
342                     {
343                         throw new GtuException("Unsupported conflict rule encountered while approaching conflicts.");
344                     }
345                 }
346             }
347 
348             // stop if required, account for upstream conflicts to keep clear
349             if (stop)
350             {
351                 prevStarts.add(conflict.getDistance());
352                 conflictRuleTypes.add(conflict.getConflictRuleType());
353 
354                 // stop for first conflict looking upstream of this blocked conflict that allows sufficient space
355                 int j = 0; // most upstream conflict if not in between conflicts
356                 for (int i = prevEnds.size() - 1; i >= 0; i--) // downstream to upstream
357                 {
358                     // note, at this point prevStarts contains one more conflict than prevEnds
359                     if (prevStarts.get(i + 1).minus(prevEnds.get(i))
360                             .gt(passableDistance(context.getLength(), context.getParameters())))
361                     {
362                         j = i + 1;
363                         break;
364                     }
365                 }
366                 if (blocking && j == 0)
367                 {
368                     // we are blocking a conflict, let's not stop more upstream than the conflict that forces our stop
369                     j = prevStarts.size() - 1;
370                 }
371 
372                 // stop for j'th conflict, if deceleration is too strong, for next one
373                 context.getParameters().setParameterResettable(S0, context.getParameters().getParameter(S0_CONF));
374                 Acceleration bCrit = context.getParameters().getParameter(ParameterTypes.BCRIT).neg();
375                 Acceleration aConflict = Acceleration.ofSI(-Double.MAX_VALUE);
376                 while (aConflict.si < bCrit.si && j < prevStarts.size())
377                 {
378                     if (prevStarts.get(j).lt(context.getParameters().getParameter(S0_CONF)))
379                     {
380                         // critical deceleration once GTU is within s0_conf
381                         // otherwise car-following model may generate unreasonably large decelerations
382                         aConflict = Acceleration.max(aConflict, bCrit);
383                     }
384                     else
385                     {
386                         Acceleration aStop = CarFollowingUtil.stop(context, prevStarts.get(j));
387                         if (conflictRuleTypes.get(j).equals(BusStopConflictRule.class) && aStop.lt(bCrit))
388                         {
389                             // as it may suddenly switch state, i.e. ignore like a yellow traffic light
390                             aStop = Acceleration.POS_MAXVALUE;
391                         }
392                         aConflict = Acceleration.max(aConflict, aStop);
393                     }
394                     j++;
395                 }
396                 context.getParameters().resetParameter(S0);
397                 a = Acceleration.min(a, aConflict);
398                 break;
399             }
400 
401             // remember info to keep conflict clear (when stopping for another conflict)
402             if (conflict.isCrossing())
403             {
404                 prevStarts.add(conflict.getDistance());
405                 conflictRuleTypes.add(conflict.getConflictRuleType());
406                 prevEnds.add(conflict.getDistance().plus(conflict.getLength()));
407             }
408         }
409         conflictPlans.setBlocking(blocking);
410 
411         if (a.si < -6.0 && context.getSpeed().si > 5.0 / 3.6)
412         {
413             Logger.ots().info("Deceleration from conflict util stronger than 6m/s^2.");
414             // return Acceleration.POSITIVE_INFINITY;
415         }
416         return a;
417     }
418 
419     /**
420      * Determines acceleration for following conflicting vehicles <i>on</i> a merge or split conflict.
421      * @param context tactical information such as parameters and car-following model
422      * @param conflict merge or split conflict
423      * @return acceleration for following conflicting vehicles <i>on</i> a merge or split conflict
424      * @throws ParameterException if a parameter is not given or out of bounds
425      */
426     private static Acceleration followConflictingLeaderOnMergeOrSplit(final TacticalContext context,
427             final PerceivedConflict conflict) throws ParameterException
428     {
429         // ignore if no conflicting GTU's, or if first is downstream of conflict
430         PerceptionIterable<PerceivedGtu> downstreamGTUs = conflict.getDownstreamConflictingGtus();
431         if (downstreamGTUs.isEmpty() || downstreamGTUs.first().getKinematics().getOverlap().isAhead())
432         {
433             return Acceleration.POS_MAXVALUE;
434         }
435         // get the most upstream GTU to consider
436         PerceivedGtu c = null;
437         Length virtualDistance = null;
438         if (conflict.getDistance().gt0())
439         {
440             c = downstreamGTUs.first();
441             virtualDistance = getVirtualDistance(c, conflict);
442         }
443         else
444         {
445             for (PerceivedGtu con : downstreamGTUs)
446             {
447                 if (con.getKinematics().getOverlap().isAhead())
448                 {
449                     // conflict GTU completely downstream of conflict (i.e. regular car-following, ignore here)
450                     return Acceleration.POS_MAXVALUE;
451                 }
452                 // conflict GTU (partially) on the conflict
453                 virtualDistance = getVirtualDistance(con, conflict);
454                 if (virtualDistance.gt0())
455                 {
456                     // on split, ignore leader if combined vehicle widths fit within the width of the conflict
457                     if (conflict.isSplit())
458                     {
459                         double conflictWidth = conflict.getWidthAtFraction(
460                                 (-conflict.getDistance().si + virtualDistance.si) / conflict.getConflictingLength().si).si;
461                         double gtuWidth = con.getWidth().si + context.getWidth().si;
462                         if (conflictWidth > gtuWidth)
463                         {
464                             continue;
465                         }
466                     }
467                     // found first downstream GTU on conflict
468                     c = con;
469                     break;
470                 }
471             }
472         }
473         if (c == null)
474         {
475             // conflict GTU downstream of start of conflict, but upstream of us
476             return Acceleration.POS_MAXVALUE;
477         }
478         // follow leader
479         Acceleration a = CarFollowingUtil.followSingleLeader(context, virtualDistance, c.getSpeed());
480         // if conflicting GTU is partially upstream of the conflict and at (near) stand-still, stop for the conflict rather than
481         // following the tail of the conflicting GTU
482         if (conflict.isMerge() && virtualDistance.lt(conflict.getDistance()))
483         {
484             /*-
485              * ______________________________________________
486              *    ___    stop for conflict  |       |
487              *   |___|(--------------------)|   ___ |
488              * _____________________________|__/  /_|________
489              *                              / /__/  /
490              *                             /       /
491              */
492             context.getParameters().setParameterResettable(S0, context.getParameters().getParameter(S0_CONF));
493             Acceleration aStop = CarFollowingUtil.stop(context, conflict.getDistance());
494             context.getParameters().resetParameter(S0);
495             a = Acceleration.max(a, aStop); // max, which ever allows the largest acceleration
496         }
497         return a;
498     }
499 
500     /**
501      * Set lateral deviation intent when appropriate.
502      * @param context tactical information such as parameters and car-following model
503      * @param conflict conflict (need not be a split, this is checked)
504      * @param leaders leaders
505      * @param availableSpace distance over which movement is expected to be possible
506      */
507     private static void lateralDeviationAtSplit(final TacticalContextEgo context, final PerceivedConflict conflict,
508             final PerceptionCollectable<PerceivedGtu, LaneBasedGtu> leaders, final Length availableSpace)
509     {
510         // 1) This only concerns splits
511         // 2) In rare cases it might be geometrically ambiguous which side to move to, so skip then
512         // 3) Only if DEV_SPLIT enabled
513         // 4) Upstream of conflict-pair on same link it would arbitrarily result in deviating left or right, so skip then
514         // (this is due to both conflicts being perceived as they are both on the current lane and on the route)
515         if (conflict.isSplit() && !conflict.getTurn().isNone()
516                 && context.getParameters().getOptionalParameter(DEV_SPLIT).orElse(false)
517                 && (!conflict.getLane().getLink().equals(conflict.getConflictingLink()) || conflict.getDistance().lt0()))
518         {
519 
520             // distance at which the deviation should be achieved
521             Length relevantDistance = null;
522 
523             // first leader in ego-direction (but beyond start of the conflict)
524             if (!leaders.isEmpty() && leaders.first().getDistance().gt(conflict.getDistance()))
525             {
526                 relevantDistance = leaders.first().getDistance();
527             }
528             // or first conflicting vehicle
529             var conflictingLeaders = conflict.getDownstreamConflictingGtus();
530             if (!conflictingLeaders.isEmpty())
531             {
532                 Length virtualDistance = getVirtualDistance(conflictingLeaders.first(), conflict);
533                 relevantDistance = relevantDistance == null ? virtualDistance : Length.min(relevantDistance, virtualDistance);
534             }
535 
536             // 1) there must be at least some leading vehicle somewhere
537             // 2a) standstill on the conflict is likely, or
538             // 2b) for the case the other direction might cause a vehicle to stand still (which we know nothing about), deviate
539             // if the relevant distance (distance to some leading vehicle)
540             if (relevantDistance != null && (conflict.getDistance().plus(conflict.getLength()).gt(availableSpace)
541                     || (relevantDistance.lt(availableSpace) && !conflictingLeaders.isEmpty()
542                             && conflictingLeaders.first().getSpeed().eq0())))
543             {
544                 // deviation based on lane and vehicle width
545                 Length positionAtWidthToConsider = Length.max(Length.ZERO, conflict.getDistance().neg());
546                 Length laneWidth = conflict.getLane().getWidth(positionAtWidthToConsider);
547                 Length deviation = laneWidth.times(0.5).minus(context.getWidth().times(0.5));
548                 if (conflict.getTurn().isRight())
549                 {
550                     deviation = deviation.neg();
551                 }
552 
553                 // never before conflict itself
554                 context.addIntent(deviation, Length.max(Length.ZERO, relevantDistance));
555             }
556         }
557     }
558 
559     /**
560      * Returns the virtual distance towards a conflicting vehicle on a merge or split conflict. Results on a crossing conflict
561      * make no sense and this method should not be called on such conflicts.
562      * @param conflictingVehicle conflicting vehicle
563      * @param conflict conflict
564      * @return virtual distance towards a conflicting vehicle on a merge or split conflict
565      */
566     private static Length getVirtualDistance(final PerceivedGtu conflictingVehicle, final PerceivedConflict conflict)
567     {
568         if (conflictingVehicle.getKinematics().getOverlap().isAhead())
569         {
570             return conflict.getDistance().plus(conflict.getLength()).plus(conflictingVehicle.getDistance());
571         }
572         if (conflictingVehicle.getKinematics().getOverlap().isBehind())
573         {
574             return conflict.getDistance().minus(conflictingVehicle.getDistance()).minus(conflictingVehicle.getLength());
575         }
576         /*-
577          * ______________________________________________
578          *   ___      virtual headway   |  ___  |
579          *  |___|(-----------------------)|___|(vehicle from south, on lane from south, but virtually on lane from west)
580          * _____________________________|_______|________
581          *                              /       /
582          *                             /       /
583          */
584         return conflict.getDistance().plus(conflictingVehicle.getKinematics().getOverlap().getOverlapRear().get());
585     }
586 
587     /**
588      * Determines an acceleration required to avoid a collision with GTUs <i>on</i> a crossing conflict.
589      * @param context tactical information such as parameters and car-following model
590      * @param conflict conflict
591      * @return acceleration required to avoid a collision
592      * @throws ParameterException if parameter is not defined
593      */
594     private static Acceleration avoidCrossingCollision(final TacticalContext context, final PerceivedConflict conflict)
595             throws ParameterException
596     {
597         // gather relevant GTUs (first up, and downstream on)
598         List<PerceivedGtu> conflictingGTUs = new ArrayList<>();
599         for (PerceivedGtu gtu : conflict.getUpstreamConflictingGtus())
600         {
601             if (conflict.getConflictingVisibility().lt(gtu.getDistance()))
602             {
603                 break;
604             }
605             if (isOnRoute(conflict.getConflictingLink(), gtu))
606             {
607                 // first upstream vehicle on route to this conflict
608                 conflictingGTUs.add(gtu);
609                 break;
610             }
611         }
612         for (PerceivedGtu gtu : conflict.getDownstreamConflictingGtus())
613         {
614             if (gtu.getKinematics().getOverlap().isParallel())
615             {
616                 conflictingGTUs.add(gtu);
617             }
618             else
619             {
620                 // vehicles beyond conflict are not a thread
621                 break;
622             }
623         }
624 
625         if (conflictingGTUs.isEmpty())
626         {
627             return Acceleration.POS_MAXVALUE;
628         }
629 
630         Acceleration a = Acceleration.POS_MAXVALUE;
631         for (PerceivedGtu conflictingGTU : conflictingGTUs)
632         {
633             // time till enter, conflicting vehicle, no acceleration
634             AnticipationInfo tteCz;
635             Length distance;
636             if (conflictingGTU.getKinematics().getOverlap().isParallel())
637             {
638                 tteCz = new AnticipationInfo(Duration.ZERO, conflictingGTU.getSpeed());
639                 distance = conflictingGTU.getKinematics().getOverlap().getOverlapRear().get().abs()
640                         .plus(conflictingGTU.getKinematics().getOverlap().getOverlap().get()).plus(Length.max(Length.ZERO,
641                                 conflictingGTU.getKinematics().getOverlap().getOverlapFront().get().neg()));
642             }
643             else
644             {
645                 tteCz = AnticipationInfo.anticipateMovement(conflictingGTU.getDistance(), conflictingGTU.getSpeed(),
646                         Acceleration.ZERO);
647                 distance = conflictingGTU.getDistance().plus(conflict.getLength()).plus(conflictingGTU.getLength());
648             }
649             // time till clear (rear past conflict), conflicting vehicle, no acceleration
650             AnticipationInfo ttcCz =
651                     AnticipationInfo.anticipateMovement(distance, conflictingGTU.getSpeed(), Acceleration.ZERO);
652             // time till enter, own vehicle, free acceleration
653             AnticipationInfo tteOa =
654                     AnticipationInfo.anticipateMovementFreeAcceleration(context, conflict.getDistance(), TIME_STEP);
655             // enter before cleared (tteCz < tteOa < ttcCz)
656             // TODO safety factor?
657             if (tteCz.duration().lt(tteOa.duration()) && tteOa.duration().lt(ttcCz.duration()))
658             {
659                 if (!conflictingGTU.getSpeed().eq0() || !CROSSSTANDING)
660                 {
661                     double t = ttcCz.duration().si;
662                     // solve parabolic speed profile s = v*t + .5*a*t*t, a =
663                     double acc = 2.0 * (conflict.getDistance().si - context.getSpeed().si * t) / (t * t);
664                     // time till zero speed > time to avoid conflict?
665                     if (context.getSpeed().si / -acc > ttcCz.duration().si)
666                     {
667                         a = Acceleration.min(a, new Acceleration(acc, AccelerationUnit.SI));
668                     }
669                     else
670                     {
671                         // will reach zero speed ourselves
672                         a = Acceleration.min(a, CarFollowingUtil.stop(context, conflict.getDistance()));
673                     }
674                 }
675                 else
676                 {
677                     // conflicting vehicle stand-still, ignore even at conflict
678                     if (tteOa.duration()
679                             .lt(context.getParameters().getOptionalParameter(ParameterTypes.DT).orElse(Duration.ofSI(0.5))))
680                     {
681                         // report only if the GTU is likely to enter the conflict in the next time step
682                         Map<String, Set<String>> map = CROSSEVENTS.get();
683                         if (map.computeIfAbsent(context.getId(), (id) -> new LinkedHashSet<>()).add(conflictingGTU.getId()))
684                         {
685                             int count = map.values().stream().reduce(0, (c, s) -> Integer.valueOf(c + s.size()),
686                                     (c1, c2) -> Integer.valueOf(c1 + c2));
687                             Logger.ots().info("GTU {} passes through GTU {} at crossing [{}].", context.getId(),
688                                     conflictingGTU.getId(), count);
689                         }
690                     }
691                 }
692             }
693         }
694         return a;
695     }
696 
697     /**
698      * Avoid collision at merge. This method assumes the GTU has priority.
699      * @param context tactical information such as parameters and car-following model
700      * @param conflict conflict
701      * @return acceleration required to avoid a collision
702      * @throws ParameterException if parameter is not defined
703      */
704     private static Acceleration avoidMergeCollision(final TacticalContext context, final PerceivedConflict conflict)
705             throws ParameterException
706     {
707         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> conflicting = conflict.getUpstreamConflictingGtus();
708         // parallel, followConflictingLeaderOnMergeOrSplit?
709         if (conflicting.isEmpty() || conflicting.first().getKinematics().getOverlap().isParallel())
710         {
711             return Acceleration.POS_MAXVALUE;
712         }
713         // TODO: this check is simplistic, designed quick and dirty, it just adds 3s as a safe gap
714         PerceivedGtu conflictingGtu = conflicting.first();
715         double tteC = conflictingGtu.getDistance().si / conflictingGtu.getSpeed().si;
716         if (tteC < conflict.getDistance().si / context.getSpeed().si + 3.0)
717         {
718             return CarFollowingUtil.stop(context, conflict.getDistance());
719         }
720         return Acceleration.POS_MAXVALUE;
721     }
722 
723     /**
724      * Approach a give-way conflict.
725      * @param context tactical information such as parameters and car-following model
726      * @param conflict conflict
727      * @param leaders leaders
728      * @param bType parameter type for considered deceleration
729      * @param prevEnd distance to end of previous conflict that should not be blocked, {@code null} if none
730      * @return whether to stop for this conflict
731      * @throws ParameterException if a parameter is not defined
732      */
733     @SuppressWarnings({"checkstyle:parameternumber", "checkstyle:methodlength"})
734     public static boolean stopForGiveWayConflict(final TacticalContext context, final PerceivedConflict conflict,
735             final PerceptionCollectable<PerceivedGtu, LaneBasedGtu> leaders, final ParameterTypeAcceleration bType,
736             final Length prevEnd) throws ParameterException
737     {
738         // Account for limited visibility and traffic light
739         PerceptionCollectable<PerceivedGtu, LaneBasedGtu> conflictingVehiclesCollectable =
740                 conflict.getUpstreamConflictingGtus();
741         Iterable<PerceivedGtu> conflictingVehicles;
742         if (conflictingVehiclesCollectable.isEmpty())
743         {
744             if (conflict.getConflictingTrafficLightDistance().isEmpty())
745             {
746                 // none within visibility, assume a conflicting vehicle just outside of visibility driving at speed limit
747                 Length length = Length.ofSI(4.0);
748                 PerceivedGtuSimple conflictGtu = new PerceivedGtuSimple("virtual " + UUID.randomUUID().toString(),
749                         DefaultsNl.CAR, length, Length.ofSI(2.0),
750                         Kinematics.dynamicBehind(conflict.getConflictingVisibility(),
751                                 conflict.getConflictingSpeedLimit().speed(), Acceleration.ZERO, true, length,
752                                 conflict.getLength()),
753                         Signals.NONE, Maneuver.NONE);
754                 conflictingVehicles = Set.of(conflictGtu);
755             }
756             else
757             {
758                 // no conflicting vehicles
759                 return false;
760             }
761         }
762         else
763         {
764             PerceivedGtu conflicting = conflictingVehiclesCollectable.first();
765             Optional<Length> tlDistance = conflict.getConflictingTrafficLightDistance();
766             if (tlDistance.isPresent() && conflicting.getKinematics().getOverlap().isAhead()
767                     && tlDistance.get().lt(conflicting.getDistance())
768                     && (conflicting.getSpeed().eq0() || conflicting.getAcceleration().lt0()))
769             {
770                 // conflicting traffic upstream of traffic light
771                 return false;
772             }
773             conflictingVehicles = conflictingVehiclesCollectable;
774         }
775 
776         // Get data independent of conflicting vehicle
777         Acceleration b = context.getParameters().getParameter(bType).neg();
778         double f = context.getParameters().getParameter(TIME_FACTOR);
779         Duration gap = context.getParameters().getParameter(MIN_GAP);
780         Length passable = passableDistance(context.getLength(), context.getParameters());
781         Length distance = conflict.getDistance().plus(context.getLength());
782         if (conflict.isCrossing())
783         {
784             distance = distance.plus(conflict.getLength()); // merge is cleared at start, crossing at end
785         }
786 
787         // time till clear (i.e. rear leaves conflict), own vehicle, free acceleration
788         AnticipationInfo ttcOa = AnticipationInfo.anticipateMovementFreeAcceleration(context, distance, TIME_STEP);
789 
790         // Loop over conflicting vehicles
791         boolean first = true;
792         for (PerceivedGtu conflictingVehicle : conflictingVehicles)
793         {
794             // skip if not on route
795             if (!isOnRoute(conflict.getConflictingLink(), conflictingVehicle))
796             {
797                 continue;
798             }
799 
800             // do not stop if first conflicting vehicle is standing still
801             if (first && conflictingVehicle.getSpeed().eq0() && conflictingVehicle.getKinematics().getOverlap().isAhead())
802             {
803                 return false;
804             }
805 
806             // time till enter, conflict vehicle, free acceleration
807             AnticipationInfo tteCa;
808             if (conflictingVehicle instanceof PerceivedGtuSimple)
809             {
810                 // fixed acceleration for simple as it provides no behavioral information
811                 tteCa = AnticipationInfo.anticipateMovement(conflictingVehicle.getDistance(), conflictingVehicle.getSpeed(),
812                         conflictingVehicle.getAcceleration());
813             }
814             else
815             {
816                 // Constant acceleration creates inf at stand still, triggering passing trough a congested stream
817                 if (conflictingVehicle.getKinematics().getOverlap().isAhead())
818                 {
819                     tteCa = AnticipationInfo.anticipateMovementFreeAcceleration(conflictingVehicle,
820                             conflictingVehicle.getDistance(), TIME_STEP);
821                 }
822                 else
823                 {
824                     tteCa = new AnticipationInfo(Duration.ZERO, conflictingVehicle.getSpeed());
825                 }
826             }
827 
828             // check gap
829             if (conflict.isMerge())
830             {
831                 /*
832                  * At a merge the conflicting vehicle will become ego's follower. Hence time might be needed to overcome a speed
833                  * difference. We assume that the speed difference at the moment the ego vehicle clears the conflict, relative
834                  * to the current speed of the conflicting vehicle, must be removed by deceleration b. This will require
835                  * additional time, within which we need to have moved sufficiently assuming the speed we have when we clear the
836                  * conflict. Sufficient space is when the follower can perform the deceleration over that space. Then, also
837                  * space for a car-following headway and the gap time is required. Note that tteCa assumes free acceleration of
838                  * the conflicting vehicle until it enters the conflict. The ego vehicle however clears the conflict earlier, so
839                  * the resulting speed of that acceleration is not the right speed to consider for the speed difference after
840                  * the conflict. The conflicting vehicle's speed at the moment ego clear the conflict is not known. Hence, the
841                  * current conflicting vehicle's speed is used. This is compensated by not assuming further acceleration of ego
842                  * after clearing the conflict.
843                  */
844                 double vSelf = ttcOa.endSpeed().si;
845                 double speedDiff = conflictingVehicle.getSpeed().si - vSelf;
846                 speedDiff = speedDiff > 0 ? speedDiff : 0;
847                 Duration additionalTime = Duration.ofSI(speedDiff / -b.si);
848                 double followerFront = conflictingVehicle.getSpeed().si * (ttcOa.duration().si + additionalTime.si)
849                         - conflictingVehicle.getDistance().si + 0.5 * b.si * additionalTime.si * additionalTime.si;
850                 double ownRear = vSelf * additionalTime.si;
851                 Duration tMax = context.getParameters().getParameter(ParameterTypes.TMAX);
852                 Length s0 = context.getParameters().getParameter(S0);
853                 // 1) will clear the conflict after the conflict vehicle enters
854                 // 2) conflict vehicle will be too near after adjusting speed
855                 if (ttcOa.duration().times(f).plus(gap).gt(tteCa.duration()) || (!Double.isInfinite(tteCa.duration().si)
856                         && tteCa.duration().si > 0.0 && ownRear < (followerFront + (tMax.si + gap.si) * vSelf + s0.si) * f))
857                 {
858                     return true;
859                 }
860             }
861             else if (conflict.isCrossing())
862             {
863                 // time till passible, downstream, zero acceleration
864                 AnticipationInfo ttpDz = null;
865                 if (!leaders.isEmpty())
866                 {
867                     distance = conflict.getDistance().minus(leaders.first().getDistance()).plus(conflict.getLength())
868                             .plus(passable);
869                     ttpDz = AnticipationInfo.anticipateMovement(distance, leaders.first().getSpeed(), Acceleration.ZERO);
870                 }
871                 else
872                 {
873                     // no leader so conflict is passable within a duration of 0
874                     ttpDz = new AnticipationInfo(Duration.ZERO, Speed.ZERO);
875                 }
876                 // 1) downstream vehicle must supply sufficient space before conflict vehicle will enter
877                 // 2) must clear the conflict before the conflict vehicle will enter
878                 if (ttpDz.duration().times(f).plus(gap).gt(tteCa.duration())
879                         || ttcOa.duration().times(f).plus(gap).gt(tteCa.duration()))
880                 {
881                     return true;
882                 }
883             }
884             else
885             {
886                 throw new RuntimeException(
887                         "Conflict is of unknown type " + conflict.getConflictType() + ", which is not merge nor a crossing.");
888             }
889             first = false;
890         }
891 
892         // No conflict vehicle triggered stopping
893         return false;
894     }
895 
896     /**
897      * Approach a stop conflict. Currently this is equal to approaching a give-way conflict.
898      * @param context tactical information such as parameters and car-following model
899      * @param conflict conflict
900      * @param leaders leaders
901      * @param bType parameter type for considered deceleration
902      * @param prevEnd distance to end of previous conflict that should not be blocked, {@code null} if none
903      * @return whether to stop for this conflict
904      * @throws ParameterException if a parameter is not defined
905      */
906     @SuppressWarnings("checkstyle:parameternumber")
907     public static boolean stopForStopConflict(final TacticalContext context, final PerceivedConflict conflict,
908             final PerceptionCollectable<PerceivedGtu, LaneBasedGtu> leaders, final ParameterTypeAcceleration bType,
909             final Length prevEnd) throws ParameterException
910     {
911         // TODO stopping
912         return stopForGiveWayConflict(context, conflict, leaders, bType, prevEnd);
913     }
914 
915     /**
916      * Approach an all-stop conflict.
917      * @param conflict conflict to approach
918      * @param conflictPlans set of plans for conflict
919      * @return whether to stop for this conflict
920      */
921     public static boolean stopForAllStopConflict(final PerceivedConflict conflict, final ConflictPlans conflictPlans)
922     {
923         // TODO all-stop behavior
924         if (conflictPlans.isStopPhaseRun(conflict.getStopLine()))
925         {
926             return false;
927         }
928         return false;
929     }
930 
931     /**
932      * Returns whether the conflicting link is on the route of the given gtu.
933      * @param conflictingLink conflicting link
934      * @param gtu gtu
935      * @return whether the conflict is on the route of the given gtu
936      */
937     private static boolean isOnRoute(final CrossSectionLink conflictingLink, final PerceivedGtu gtu)
938     {
939         try
940         {
941             Optional<Route> route = gtu.getBehavior().getRoute();
942             if (route.isEmpty())
943             {
944                 // conservative assumption: it's on the route (gtu should be upstream of the conflict)
945                 return true;
946             }
947             Node startNode = conflictingLink.getStartNode();
948             Node endNode = conflictingLink.getEndNode();
949             return route.get().contains(startNode) && route.get().contains(endNode)
950                     && Math.abs(route.get().indexOf(endNode) - route.get().indexOf(startNode)) == 1;
951         }
952         catch (UnsupportedOperationException uoe)
953         {
954             // conservative assumption: it's on the route (gtu should be upstream of the conflict)
955             return true;
956         }
957     }
958 
959     /**
960      * Returns distance needed behind the leader to completely pass the conflict.
961      * @param vehicleLength vehicle length
962      * @param parameters parameters
963      * @return distance needed behind the leader to completely pass the conflict
964      * @throws ParameterException if parameter is not available
965      */
966     private static Length passableDistance(final Length vehicleLength, final Parameters parameters) throws ParameterException
967     {
968         return parameters.getParameter(S0).plus(vehicleLength);
969     }
970 
971     /**
972      * Holds the tactical plans of a driver considering conflicts. These are remembered for consistency. For instance, if the
973      * decision is made to yield as current deceleration suggests it's safe to do so, but the trajectory for stopping in front
974      * of the conflict results in deceleration slightly above what is considered safe deceleration, the plan should not be
975      * abandoned. Decelerations above what is considered safe deceleration may result due to numerical overshoot or other
976      * factors coming into play in car-following models. Many other examples exist where a driver sticks to a certain plan.
977      */
978     public static final class ConflictPlans implements Blockable
979     {
980 
981         /** Phases of navigating an all-stop intersection per intersection. */
982         private final LinkedHashMap<String, StopPhase> stopPhases = new LinkedHashMap<>();
983 
984         /** Estimated arrival times of vehicles at all-stop intersection. */
985         private final LinkedHashMap<String, Time> arrivalTimes = new LinkedHashMap<>();
986 
987         /** Whether the GTU is blocking conflicts. */
988         private boolean blocking;
989 
990         /** Id of GTU that we allow priority (although it has not) for zip-merging at congested merge conflict. */
991         private String zipGtuId;
992 
993         /**
994          * Constructor.
995          */
996         public ConflictPlans()
997         {
998             //
999         }
1000 
1001         /**
1002          * Sets the estimated arrival time of a GTU.
1003          * @param gtu GTU
1004          * @param time estimated arrival time
1005          */
1006         void setArrivalTime(final PerceivedGtuBase gtu, final Time time)
1007         {
1008             this.arrivalTimes.put(gtu.getId(), time);
1009         }
1010 
1011         /**
1012          * Returns the estimated arrival time of given GTU.
1013          * @param gtu GTU
1014          * @return estimated arrival time of given GTU
1015          */
1016         Time getArrivalTime(final PerceivedGtuBase gtu)
1017         {
1018             return this.arrivalTimes.get(gtu.getId());
1019         }
1020 
1021         /**
1022          * Sets the current phase to 'approach' for the given stop line.
1023          * @param stopLine stop line
1024          */
1025         void setStopPhaseApproach(final PerceivedObject stopLine)
1026         {
1027             this.stopPhases.put(stopLine.getId(), StopPhase.APPROACH);
1028         }
1029 
1030         /**
1031          * Sets the current phase to 'yield' for the given stop line.
1032          * @param stopLine stop line
1033          * @throws OtsRuntimeException if the phase was not set to approach before
1034          */
1035         void setStopPhaseYield(final PerceivedObject stopLine)
1036         {
1037             Throw.when(
1038                     !this.stopPhases.containsKey(stopLine.getId())
1039                             || !this.stopPhases.get(stopLine.getId()).equals(StopPhase.APPROACH),
1040                     OtsRuntimeException.class, "Yield stop phase is set for stop line that was not approached.");
1041             this.stopPhases.put(stopLine.getId(), StopPhase.YIELD);
1042         }
1043 
1044         /**
1045          * Sets the current phase to 'run' for the given stop line.
1046          * @param stopLine stop line
1047          * @throws OtsRuntimeException if the phase was not set to approach before
1048          */
1049         void setStopPhaseRun(final PerceivedObject stopLine)
1050         {
1051             Throw.when(!this.stopPhases.containsKey(stopLine.getId()), OtsRuntimeException.class,
1052                     "Run stop phase is set for stop line that was not approached.");
1053             this.stopPhases.put(stopLine.getId(), StopPhase.YIELD);
1054         }
1055 
1056         /**
1057          * Return whether plan is in approach stop line phase.
1058          * @param stopLine stop line
1059          * @return whether the current phase is 'approach' for the given stop line
1060          */
1061         boolean isStopPhaseApproach(final PerceivedObject stopLine)
1062         {
1063             return this.stopPhases.containsKey(stopLine.getId())
1064                     && this.stopPhases.get(stopLine.getId()).equals(StopPhase.APPROACH);
1065         }
1066 
1067         /**
1068          * Returns whether yielding was planned for the stop line.
1069          * @param stopLine stop line
1070          * @return whether the current phase is 'yield' for the given stop line
1071          */
1072         boolean isStopPhaseYield(final PerceivedObject stopLine)
1073         {
1074             return this.stopPhases.containsKey(stopLine.getId())
1075                     && this.stopPhases.get(stopLine.getId()).equals(StopPhase.YIELD);
1076         }
1077 
1078         /**
1079          * Returns whether running was planned for the stop line.
1080          * @param stopLine stop line
1081          * @return whether the current phase is 'run' for the given stop line
1082          */
1083         boolean isStopPhaseRun(final PerceivedObject stopLine)
1084         {
1085             return this.stopPhases.containsKey(stopLine.getId()) && this.stopPhases.get(stopLine.getId()).equals(StopPhase.RUN);
1086         }
1087 
1088         @Override
1089         public boolean isBlocking()
1090         {
1091             return this.blocking;
1092         }
1093 
1094         /**
1095          * Sets the GTU as blocking conflicts or not.
1096          * @param blocking whether the GTU is blocking conflicts
1097          */
1098         public void setBlocking(final boolean blocking)
1099         {
1100             this.blocking = blocking;
1101         }
1102 
1103         /**
1104          * Sets the id of GTU that we allow priority (although it has not) for zip-merging at congested merge conflict.
1105          * @param zipGtuId id of GTU that we allow priority
1106          */
1107         void setZipGtu(final String zipGtuId)
1108         {
1109             this.zipGtuId = zipGtuId;
1110         }
1111 
1112         /**
1113          * Clear the id of GTU that we allow priority (although it has not) for zip-merging at congested merge conflict.
1114          */
1115         void clearZipGtu()
1116         {
1117             this.zipGtuId = null;
1118         }
1119 
1120         /**
1121          * Check whether the given id is of the GTU for which we allow priority.
1122          * @param id GTU id
1123          * @return whether the given id is of the GTU for which we allow priority
1124          */
1125         boolean isZipGtu(final String id)
1126         {
1127             return id.equals(this.zipGtuId);
1128         }
1129 
1130         @Override
1131         public String toString()
1132         {
1133             return "ConflictPlans";
1134         }
1135 
1136     }
1137 
1138     /**
1139      * Phases of navigating an all-stop intersection.
1140      * <p>
1141      * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1142      * <br>
1143      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
1144      * </p>
1145      * @author Alexander Verbraeck
1146      * @author Peter Knoppers
1147      * @author Wouter Schakel
1148      */
1149     private enum StopPhase
1150     {
1151         /** Approaching stop intersection. */
1152         APPROACH,
1153 
1154         /** Yielding for stop intersection. */
1155         YIELD,
1156 
1157         /** Running over stop intersection. */
1158         RUN;
1159     }
1160 
1161     /**
1162      * Returns the net available space when all moving GTUs will become stationary behind the first stationary GTU.
1163      */
1164     private static final class AvailableSpace implements PerceptionCollector<Space, LaneBasedGtu, Space>
1165     {
1166         @Override
1167         public Supplier<Space> getIdentity()
1168         {
1169             return () -> new Space();
1170         }
1171 
1172         @Override
1173         public PerceptionAccumulator<LaneBasedGtu, Space> getAccumulator()
1174         {
1175             return (i, u, h) ->
1176             {
1177                 if (i.getObject().addVehicle(u, h))
1178                 {
1179                     i.stop();
1180                 }
1181                 return i;
1182             };
1183         }
1184 
1185         @Override
1186         public Function<Space, Space> getFinalizer()
1187         {
1188             return (l) -> l;
1189         }
1190     }
1191 
1192     /**
1193      * Intermediate result for {@link AvailableSpace} collector.
1194      */
1195     private static final class Space
1196     {
1197         /** Accumulated required space for vehicles up to stationary leader. */
1198         private Length cumulativeRequiredSpace = Length.ZERO;
1199 
1200         /** Distance to first stationary leader. */
1201         private Length firstStationary;
1202 
1203         /**
1204          * Adds a vehicle.
1205          * @param gtu GTU
1206          * @param h distance to GTU
1207          * @return whether the accumulator can stop as the vehicles is stationary
1208          */
1209         public boolean addVehicle(final LaneBasedGtu gtu, final Length h)
1210         {
1211             if (gtu.getSpeed().si < 5.0 / 3.6 && gtu.getAcceleration().le0())
1212             {
1213                 this.firstStationary = h;
1214                 return true;
1215             }
1216             Length s0;
1217             try
1218             {
1219                 s0 = gtu.getParameters().getParameter(ParameterTypes.S0);
1220             }
1221             catch (ParameterException ex)
1222             {
1223                 s0 = Length.ofSI(3.0);
1224             }
1225             this.cumulativeRequiredSpace = this.cumulativeRequiredSpace.plus(gtu.getLength()).plus(s0);
1226             return false;
1227         }
1228 
1229         /**
1230          * Returns the available space up to first stationary leader.
1231          * @return available space up to first stationary leader
1232          */
1233         public Length availableSpace()
1234         {
1235             return this.firstStationary == null ? Length.POSITIVE_INFINITY
1236                     : this.firstStationary.minus(this.cumulativeRequiredSpace);
1237         }
1238 
1239         /**
1240          * Returns the distance to first stationary leader.
1241          * @return distance to first stationary leader
1242          */
1243         public Length firstStationary()
1244         {
1245             return this.firstStationary == null ? Length.POSITIVE_INFINITY : this.firstStationary;
1246         }
1247     }
1248 
1249 }