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