View Javadoc
1   package org.opentrafficsim.road.gtu.lane.perception.categories;
2   
3   import java.util.HashMap;
4   import java.util.LinkedHashMap;
5   import java.util.LinkedHashSet;
6   import java.util.Map;
7   import java.util.Objects;
8   import java.util.Set;
9   import java.util.SortedSet;
10  import java.util.TreeSet;
11  import java.util.WeakHashMap;
12  
13  import org.djunits.value.vdouble.scalar.Length;
14  import org.djutils.exceptions.Throw;
15  import org.djutils.exceptions.Try;
16  import org.opentrafficsim.base.TimeStampedObject;
17  import org.opentrafficsim.base.parameters.ParameterException;
18  import org.opentrafficsim.core.gtu.GTUException;
19  import org.opentrafficsim.core.gtu.RelativePosition;
20  import org.opentrafficsim.core.network.LateralDirectionality;
21  import org.opentrafficsim.core.network.NetworkException;
22  import org.opentrafficsim.core.network.route.Route;
23  import org.opentrafficsim.road.gtu.lane.perception.InfrastructureLaneChangeInfo;
24  import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
25  import org.opentrafficsim.road.gtu.lane.perception.LaneStructureRecord;
26  import org.opentrafficsim.road.gtu.lane.perception.RelativeLane;
27  import org.opentrafficsim.road.network.lane.Lane;
28  import org.opentrafficsim.road.network.lane.object.sensor.SingleSensor;
29  import org.opentrafficsim.road.network.lane.object.sensor.SinkSensor;
30  import org.opentrafficsim.road.network.speed.SpeedLimitProspect;
31  import org.opentrafficsim.road.network.speed.SpeedLimitTypes;
32  
33  /**
34   * Perceives information concerning the infrastructure, including splits, lanes, speed limits and road markings. This category
35   * is optimized by cooperating closely with the {@code LaneStructure} and only updating internal information when the GTU is on
36   * a new {@code Lane}. On the {@code Lane} information is defined relative to the start, and thus easily calculated at each
37   * time.
38   * <p>
39   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
40   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
41   * <p>
42   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jul 14, 2016 <br>
43   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
44   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
45   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
46   */
47  // TODO: more than the lane speed limit and maximum vehicle speed in the speed limit prospect
48  public class DirectInfrastructurePerception extends LaneBasedAbstractPerceptionCategory implements InfrastructurePerception
49  {
50  
51      /** */
52      private static final long serialVersionUID = 20160811L;
53  
54      /** Infrastructure lane change info per relative lane. */
55      private final Map<RelativeLane, TimeStampedObject<SortedSet<InfrastructureLaneChangeInfo>>> infrastructureLaneChangeInfo =
56              new HashMap<>();
57  
58      /** Speed limit prospect per relative lane. */
59      private Map<RelativeLane, TimeStampedObject<SpeedLimitProspect>> speedLimitProspect = new HashMap<>();
60  
61      /** Legal Lane change possibilities per relative lane and lateral direction. */
62      private final Map<RelativeLane, Map<LateralDirectionality, TimeStampedObject<LaneChangePossibility>>> legalLaneChangePossibility =
63              new HashMap<>();
64  
65      /** Physical Lane change possibilities per relative lane and lateral direction. */
66      private final Map<RelativeLane, Map<LateralDirectionality, TimeStampedObject<LaneChangePossibility>>> physicalLaneChangePossibility =
67              new HashMap<>();
68  
69      /** Cross-section. */
70      private TimeStampedObject<SortedSet<RelativeLane>> crossSection;
71  
72      /** Cache for anyNextOk. */
73      private final Map<LaneStructureRecord, Boolean> anyNextOkCache = new WeakHashMap<>();
74  
75      /** Set of records with accessible end as they are cut off. */
76      private final Set<LaneStructureRecord> cutOff = new LinkedHashSet<>();
77  
78      /** Root. */
79      private LaneStructureRecord root;
80  
81      /** Lanes registered to the GTU used to check if an update is required. */
82      private Set<Lane> lanes;
83  
84      /** Route. */
85      private Route route;
86  
87      /**
88       * @param perception LanePerception; perception
89       */
90      public DirectInfrastructurePerception(final LanePerception perception)
91      {
92          super(perception);
93      }
94  
95      /** {@inheritDoc} */
96      @Override
97      public void updateAll() throws GTUException, ParameterException
98      {
99          updateCrossSection();
100         // clean-up
101         Set<RelativeLane> cs = getCrossSection();
102         this.infrastructureLaneChangeInfo.keySet().retainAll(cs);
103         this.legalLaneChangePossibility.keySet().retainAll(cs);
104         this.physicalLaneChangePossibility.keySet().retainAll(cs);
105         this.speedLimitProspect.keySet().retainAll(cs);
106         // only if required
107         LaneStructureRecord newRoot = getPerception().getLaneStructure().getRootRecord();
108         if (this.root == null || !newRoot.equals(this.root)
109                 || !this.lanes.equals(getPerception().getGtu().positions(RelativePosition.REFERENCE_POSITION).keySet())
110                 || !Objects.equals(this.route, getPerception().getGtu().getStrategicalPlanner().getRoute())
111                 || this.cutOff.stream().filter((record) -> !record.isCutOffEnd()).count() > 0)
112         {
113             this.cutOff.clear();
114             this.root = newRoot;
115             this.lanes = getPerception().getGtu().positions(RelativePosition.REFERENCE_POSITION).keySet();
116             this.route = getPerception().getGtu().getStrategicalPlanner().getRoute();
117             // TODO: this is not suitable if we change lane and consider e.g. dynamic speed signs, they will be forgotten
118             this.speedLimitProspect.clear();
119             for (RelativeLane lane : getCrossSection())
120             {
121                 updateInfrastructureLaneChangeInfo(lane);
122                 updateLegalLaneChangePossibility(lane, LateralDirectionality.LEFT);
123                 updateLegalLaneChangePossibility(lane, LateralDirectionality.RIGHT);
124                 updatePhysicalLaneChangePossibility(lane, LateralDirectionality.LEFT);
125                 updatePhysicalLaneChangePossibility(lane, LateralDirectionality.RIGHT);
126             }
127         }
128         // speed limit prospect
129         for (RelativeLane lane : getCrossSection())
130         {
131             updateSpeedLimitProspect(lane);
132         }
133         for (RelativeLane lane : getCrossSection())
134         {
135             if (!this.infrastructureLaneChangeInfo.containsKey(lane))
136             {
137                 updateInfrastructureLaneChangeInfo(lane); // new lane in cross section
138                 updateLegalLaneChangePossibility(lane, LateralDirectionality.LEFT);
139                 updateLegalLaneChangePossibility(lane, LateralDirectionality.RIGHT);
140                 updatePhysicalLaneChangePossibility(lane, LateralDirectionality.LEFT);
141                 updatePhysicalLaneChangePossibility(lane, LateralDirectionality.RIGHT);
142             }
143         }
144     }
145 
146     /** {@inheritDoc} */
147     @Override
148     public final void updateInfrastructureLaneChangeInfo(final RelativeLane lane) throws GTUException, ParameterException
149     {
150 
151         if (this.infrastructureLaneChangeInfo.containsKey(lane)
152                 && this.infrastructureLaneChangeInfo.get(lane).getTimestamp().equals(getTimestamp()))
153         {
154             // already done at this time
155             return;
156         }
157         updateCrossSection();
158 
159         // start at requested lane
160         SortedSet<InfrastructureLaneChangeInfo> resultSet = new TreeSet<>();
161         LaneStructureRecord record = getPerception().getLaneStructure().getFirstRecord(lane);
162         try
163         {
164             record = getPerception().getLaneStructure().getFirstRecord(lane);
165             if (!record.allowsRoute(getGtu().getStrategicalPlanner().getRoute(), getGtu().getGTUType()))
166             {
167                 resultSet.add(InfrastructureLaneChangeInfo.fromInaccessibleLane(record.isDeadEnd()));
168                 this.infrastructureLaneChangeInfo.put(lane, new TimeStampedObject<>(resultSet, getTimestamp()));
169                 return;
170             }
171         }
172         catch (NetworkException exception)
173         {
174             throw new GTUException("Route has no destination.", exception);
175         }
176         Map<LaneStructureRecord, InfrastructureLaneChangeInfo> currentSet = new LinkedHashMap<>();
177         Map<LaneStructureRecord, InfrastructureLaneChangeInfo> nextSet = new LinkedHashMap<>();
178         RelativePosition front = getPerception().getGtu().getFront();
179         currentSet.put(record,
180                 new InfrastructureLaneChangeInfo(0, record, front, record.isDeadEnd(), LateralDirectionality.NONE));
181         while (!currentSet.isEmpty())
182         {
183             // move lateral
184             nextSet.putAll(currentSet);
185             for (LaneStructureRecord laneRecord : currentSet.keySet())
186             {
187                 while (laneRecord.legalLeft() && !nextSet.containsKey(laneRecord.getLeft()))
188                 {
189                     InfrastructureLaneChangeInfo info =
190                             nextSet.get(laneRecord).left(laneRecord.getLeft(), front, laneRecord.getLeft().isDeadEnd());
191                     nextSet.put(laneRecord.getLeft(), info);
192                     laneRecord = laneRecord.getLeft();
193                 }
194             }
195             for (LaneStructureRecord laneRecord : currentSet.keySet())
196             {
197                 while (laneRecord.legalRight() && !nextSet.containsKey(laneRecord.getRight()))
198                 {
199                     InfrastructureLaneChangeInfo info =
200                             nextSet.get(laneRecord).right(laneRecord.getRight(), front, laneRecord.getRight().isDeadEnd());
201                     nextSet.put(laneRecord.getRight(), info);
202                     laneRecord = laneRecord.getRight();
203                 }
204             }
205             // move longitudinal
206             currentSet = nextSet;
207             nextSet = new LinkedHashMap<>();
208             InfrastructureLaneChangeInfo bestOk = null;
209             InfrastructureLaneChangeInfo bestNotOk = null;
210             boolean deadEnd = false;
211             for (LaneStructureRecord laneRecord : currentSet.keySet())
212             {
213                 boolean anyOk = Try.assign(() -> anyNextOk(laneRecord), "Route has no destination.");
214                 if (anyOk)
215                 {
216                     // add to nextSet
217                     for (LaneStructureRecord next : laneRecord.getNext())
218                     {
219                         try
220                         {
221                             if (next.allowsRoute(getGtu().getStrategicalPlanner().getRoute(), getGtu().getGTUType()))
222                             {
223                                 InfrastructureLaneChangeInfo prev = currentSet.get(laneRecord);
224                                 InfrastructureLaneChangeInfo info =
225                                         new InfrastructureLaneChangeInfo(prev.getRequiredNumberOfLaneChanges(), next, front,
226                                                 next.isDeadEnd(), prev.getLateralDirectionality());
227                                 nextSet.put(next, info);
228                             }
229                         }
230                         catch (NetworkException exception)
231                         {
232                             throw new RuntimeException("Network exception while considering route on next lane.", exception);
233                         }
234                     }
235                     // take best ok
236                     if (bestOk == null || currentSet.get(laneRecord).getRequiredNumberOfLaneChanges() < bestOk
237                             .getRequiredNumberOfLaneChanges())
238                     {
239                         bestOk = currentSet.get(laneRecord);
240                     }
241                 }
242                 else
243                 {
244                     // take best not ok
245                     deadEnd = deadEnd || currentSet.get(laneRecord).isDeadEnd();
246                     if (bestNotOk == null || currentSet.get(laneRecord).getRequiredNumberOfLaneChanges() < bestNotOk
247                             .getRequiredNumberOfLaneChanges())
248                     {
249                         bestNotOk = currentSet.get(laneRecord);
250                     }
251                 }
252 
253             }
254             if (bestOk == null)
255             {
256                 break;
257             }
258             // if there are lanes that are not okay and only -further- lanes that are ok, we need to change to one of the ok's
259             if (bestNotOk != null && bestOk.getRequiredNumberOfLaneChanges() > bestNotOk.getRequiredNumberOfLaneChanges())
260             {
261                 bestOk.setDeadEnd(deadEnd);
262                 resultSet.add(bestOk);
263             }
264             currentSet = nextSet;
265             nextSet = new LinkedHashMap<>();
266         }
267 
268         // save
269         this.infrastructureLaneChangeInfo.put(lane, new TimeStampedObject<>(resultSet, getTimestamp()));
270     }
271 
272     /**
273      * Returns whether the given record end is ok to pass. If not, a lane change is required before this end. The method will
274      * also return true if the next node is the end node of the route, if the lane is cut off due to limited perception range,
275      * or when there is a {@code SinkSensor} on the lane.
276      * @param record LaneStructureRecord; checked record
277      * @return whether the given record end is ok to pass
278      * @throws NetworkException if destination could not be obtained
279      * @throws GTUException if the GTU could not be obtained
280      */
281     private boolean anyNextOk(final LaneStructureRecord record) throws NetworkException, GTUException
282     {
283         if (record.isCutOffEnd())
284         {
285             this.cutOff.add(record);
286             return true; // always ok if cut-off
287         }
288         // check cache
289         Boolean ok = this.anyNextOkCache.get(record);
290         if (ok != null)
291         {
292             return ok;
293         }
294         // sink
295         for (SingleSensor s : record.getLane().getSensors())
296         {
297             if (s instanceof SinkSensor)
298             {
299                 this.anyNextOkCache.put(record, true);
300                 return true; // ok towards sink
301             }
302         }
303         // check destination
304         Route currentRoute = getGtu().getStrategicalPlanner().getRoute();
305         try
306         {
307             if (currentRoute != null && currentRoute.destinationNode().equals(record.getToNode()))
308             {
309                 this.anyNextOkCache.put(record, true);
310                 return true;
311             }
312         }
313         catch (NetworkException exception)
314         {
315             throw new RuntimeException("Could not determine destination node.", exception);
316         }
317         // check dead-end
318         if (record.getNext().isEmpty())
319         {
320             this.anyNextOkCache.put(record, false);
321             return false; // never ok if dead-end
322         }
323         // check if we have a route
324         if (currentRoute == null)
325         {
326             this.anyNextOkCache.put(record, true);
327             return true; // if no route assume ok, i.e. simple networks without routes
328         }
329         // finally check route
330         ok = record.allowsRouteAtEnd(currentRoute, getGtu().getGTUType());
331         this.anyNextOkCache.put(record, ok);
332         return ok;
333     }
334 
335     /** {@inheritDoc} */
336     @Override
337     public final void updateSpeedLimitProspect(final RelativeLane lane) throws GTUException, ParameterException
338     {
339         updateCrossSection();
340         checkLaneIsInCrossSection(lane);
341         TimeStampedObject<SpeedLimitProspect> tsSlp = this.speedLimitProspect.get(lane);
342         SpeedLimitProspect slp;
343         if (tsSlp != null)
344         {
345             slp = tsSlp.getObject();
346             slp.update(getGtu().getOdometer());
347         }
348         else
349         {
350             slp = new SpeedLimitProspect(getGtu().getOdometer());
351             slp.addSpeedInfo(Length.ZERO, SpeedLimitTypes.MAX_VEHICLE_SPEED, getGtu().getMaximumSpeed(), getGtu());
352         }
353         try
354         {
355             Lane laneObj = getGtu().getReferencePosition().getLane();
356             if (!slp.containsAddSource(laneObj))
357             {
358                 slp.addSpeedInfo(Length.ZERO, SpeedLimitTypes.FIXED_SIGN, laneObj.getSpeedLimit(getGtu().getGTUType()),
359                         laneObj);
360             }
361         }
362         catch (NetworkException exception)
363         {
364             throw new RuntimeException("Could not obtain speed limit from lane for perception.", exception);
365         }
366         this.speedLimitProspect.put(lane, new TimeStampedObject<>(slp, getTimestamp()));
367     }
368 
369     /** {@inheritDoc} */
370     @Override
371     public final void updateLegalLaneChangePossibility(final RelativeLane lane, final LateralDirectionality lat)
372             throws GTUException, ParameterException
373     {
374         updateLaneChangePossibility(lane, lat, true, this.legalLaneChangePossibility);
375     }
376 
377     /** {@inheritDoc} */
378     @Override
379     public final void updatePhysicalLaneChangePossibility(final RelativeLane lane, final LateralDirectionality lat)
380             throws GTUException, ParameterException
381     {
382         updateLaneChangePossibility(lane, lat, false, this.physicalLaneChangePossibility);
383     }
384 
385     /**
386      * Updates the distance over which lane changes remains legally or physically possible.
387      * @param lane RelativeLane; lane from which the lane change possibility is requested
388      * @param lat LateralDirectionality; LEFT or RIGHT, null not allowed
389      * @param legal boolean; legal, or physical otherwise
390      * @param possibilityMap
391      *            Map&lt;RelativeLane,Map&lt;LateralDirectionality,TimeStampedObject&lt;LaneChangePossibility&gt;&gt;&gt;;
392      *            Map&lt;RelativeLane,Map&lt;LateralDirectionality,TimeStampedObject&lt;LaneChangePossibility&gt;&gt;&gt;; legal
393      *            or physical possibility map
394      * @throws GTUException if the GTU was not initialized or if the lane is not in the cross section
395      * @throws ParameterException if a parameter is not defined
396      */
397     private void updateLaneChangePossibility(final RelativeLane lane, final LateralDirectionality lat, final boolean legal,
398             final Map<RelativeLane, Map<LateralDirectionality, TimeStampedObject<LaneChangePossibility>>> possibilityMap)
399             throws GTUException, ParameterException
400     {
401         updateCrossSection();
402         checkLaneIsInCrossSection(lane);
403 
404         if (possibilityMap.get(lane) == null)
405         {
406             possibilityMap.put(lane, new HashMap<>());
407         }
408         LaneStructureRecord record = getPerception().getLaneStructure().getFirstRecord(lane);
409         // check tail
410         Length tail = getPerception().getGtu().getRear().getDx();
411         while (record != null && record.getStartDistance().gt(tail) && !record.getPrev().isEmpty()
412                 && ((lat.isLeft() && record.possibleLeft(legal)) || (lat.isRight() && record.possibleRight(legal))))
413         {
414             if (record.getPrev().size() > 1)
415             {
416                 // assume not possible at a merge
417                 possibilityMap.get(lane).put(lat, new TimeStampedObject<>(
418                         new LaneChangePossibility(record.getPrev().get(0), tail, true), getTimestamp()));
419                 return;
420             }
421             else if (record.getPrev().isEmpty())
422             {
423                 // dead-end, no lane upwards prevents a lane change
424                 break;
425             }
426             record = record.getPrev().get(0);
427             if ((lat.isLeft() && !record.possibleLeft(legal)) || (lat.isRight() && !record.possibleRight(legal)))
428             {
429                 // this lane prevents a lane change for the tail
430                 possibilityMap.get(lane).put(lat,
431                         new TimeStampedObject<>(new LaneChangePossibility(record, tail, true), getTimestamp()));
432                 return;
433             }
434         }
435 
436         LaneStructureRecord prevRecord = null;
437         record = getPerception().getLaneStructure().getFirstRecord(lane);
438 
439         Length dx;
440         if ((lat.isLeft() && record.possibleLeft(legal)) || (lat.isRight() && record.possibleRight(legal)))
441         {
442             dx = getPerception().getGtu().getFront().getDx();
443             while (record != null
444                     && ((lat.isLeft() && record.possibleLeft(legal)) || (lat.isRight() && record.possibleRight(legal))))
445             {
446                 // TODO: splits
447                 prevRecord = record;
448                 record = record.getNext().isEmpty() ? null : record.getNext().get(0);
449             }
450         }
451         else
452         {
453             dx = getPerception().getGtu().getRear().getDx();
454             while (record != null
455                     && ((lat.isLeft() && !record.possibleLeft(legal)) || (lat.isRight() && !record.possibleRight(legal))))
456             {
457                 // TODO: splits
458                 prevRecord = record;
459                 record = record.getNext().isEmpty() ? null : record.getNext().get(0);
460             }
461         }
462         possibilityMap.get(lane).put(lat,
463                 new TimeStampedObject<>(new LaneChangePossibility(prevRecord, dx, true), getTimestamp()));
464     }
465 
466     /**
467      * @param lane RelativeLane; lane to check
468      * @throws GTUException if the lane is not in the cross section
469      */
470     private void checkLaneIsInCrossSection(final RelativeLane lane) throws GTUException
471     {
472         Throw.when(!getCrossSection().contains(lane), GTUException.class,
473                 "The requeasted lane %s is not in the most recent cross section.", lane);
474     }
475 
476     /** {@inheritDoc} */
477     @Override
478     public final void updateCrossSection() throws GTUException, ParameterException
479     {
480         if (this.crossSection != null && this.crossSection.getTimestamp().equals(getTimestamp()))
481         {
482             // already done at this time
483             return;
484         }
485         this.crossSection =
486                 new TimeStampedObject<>(getPerception().getLaneStructure().getExtendedCrossSection(), getTimestamp());
487     }
488 
489     /** {@inheritDoc} */
490     @Override
491     public final SortedSet<InfrastructureLaneChangeInfo> getInfrastructureLaneChangeInfo(final RelativeLane lane)
492     {
493         return this.infrastructureLaneChangeInfo.get(lane).getObject();
494     }
495 
496     /** {@inheritDoc} */
497     @Override
498     public final SpeedLimitProspect getSpeedLimitProspect(final RelativeLane lane)
499     {
500         return this.speedLimitProspect.get(lane).getObject();
501     }
502 
503     /** {@inheritDoc} */
504     @Override
505     public final Length getLegalLaneChangePossibility(final RelativeLane fromLane, final LateralDirectionality lat)
506     {
507         return this.legalLaneChangePossibility.get(fromLane).get(lat).getObject().getDistance(lat);
508     }
509 
510     /** {@inheritDoc} */
511     @Override
512     public final Length getPhysicalLaneChangePossibility(final RelativeLane fromLane, final LateralDirectionality lat)
513     {
514         return this.physicalLaneChangePossibility.get(fromLane).get(lat).getObject().getDistance(lat);
515     }
516 
517     /** {@inheritDoc} */
518     @Override
519     public final SortedSet<RelativeLane> getCrossSection()
520     {
521         return this.crossSection.getObject();
522     }
523 
524     /**
525      * Returns time stamped infrastructure lane change info of a lane. A set is returned as multiple points may force lane
526      * changes. Which point is considered most critical is a matter of driver interpretation and may change over time. This is
527      * shown below. Suppose vehicle A needs to take the off-ramp, and that behavior is that the minimum distance per required
528      * lane change determines how critical it is. First, 400m before the lane-drop, the off-ramp is critical. 300m downstream,
529      * the lane-drop is critical. Info is sorted by distance, closest first.
530      * 
531      * <pre>
532      * _______
533      * _ _A_ _\_________
534      * _ _ _ _ _ _ _ _ _
535      * _________ _ _ ___
536      *          \_______
537      *     (-)        Lane-drop: 1 lane change  in 400m (400m per lane change)
538      *     (--------) Off-ramp:  3 lane changes in 900m (300m per lane change, critical)
539      *     
540      *     (-)        Lane-drop: 1 lane change  in 100m (100m per lane change, critical)
541      *     (--------) Off-ramp:  3 lane changes in 600m (200m per lane change)
542      * </pre>
543      * 
544      * @param lane RelativeLane; relative lateral lane
545      * @return time stamped infrastructure lane change info of a lane
546      */
547     public final TimeStampedObject<SortedSet<InfrastructureLaneChangeInfo>> getTimeStampedInfrastructureLaneChangeInfo(
548             final RelativeLane lane)
549     {
550         return this.infrastructureLaneChangeInfo.get(lane);
551     }
552 
553     /**
554      * Returns the time stamped prospect for speed limits on a lane (dynamic speed limits may vary between lanes).
555      * @param lane RelativeLane; relative lateral lane
556      * @return time stamped prospect for speed limits on a lane
557      */
558     public final TimeStampedObject<SpeedLimitProspect> getTimeStampedSpeedLimitProspect(final RelativeLane lane)
559     {
560         return this.speedLimitProspect.get(lane);
561     }
562 
563     /**
564      * Returns the time stamped distance over which a lane change remains legally possible.
565      * @param fromLane RelativeLane; lane from which the lane change possibility is requested
566      * @param lat LateralDirectionality; LEFT or RIGHT, null not allowed
567      * @return time stamped distance over which a lane change remains possible
568      * @throws NullPointerException if {@code lat == null}
569      */
570     public final TimeStampedObject<Length> getTimeStampedLegalLaneChangePossibility(final RelativeLane fromLane,
571             final LateralDirectionality lat)
572     {
573         TimeStampedObject<LaneChangePossibility> tsLcp = this.legalLaneChangePossibility.get(fromLane).get(lat);
574         LaneChangePossibility lcp = tsLcp.getObject();
575         return new TimeStampedObject<>(lcp.getDistance(lat), tsLcp.getTimestamp());
576     }
577 
578     /**
579      * Returns the time stamped distance over which a lane change remains physically possible.
580      * @param fromLane RelativeLane; lane from which the lane change possibility is requested
581      * @param lat LateralDirectionality; LEFT or RIGHT, null not allowed
582      * @return time stamped distance over which a lane change remains possible
583      * @throws NullPointerException if {@code lat == null}
584      */
585     public final TimeStampedObject<Length> getTimeStampedPhysicalLaneChangePossibility(final RelativeLane fromLane,
586             final LateralDirectionality lat)
587     {
588         TimeStampedObject<LaneChangePossibility> tsLcp = this.physicalLaneChangePossibility.get(fromLane).get(lat);
589         LaneChangePossibility lcp = tsLcp.getObject();
590         return new TimeStampedObject<>(lcp.getDistance(lat), tsLcp.getTimestamp());
591     }
592 
593     /**
594      * Returns a time stamped set of relative lanes representing the cross section. Lanes are sorted left to right.
595      * @return time stamped set of relative lanes representing the cross section
596      */
597     public final TimeStampedObject<SortedSet<RelativeLane>> getTimeStampedCrossSection()
598     {
599         return this.crossSection;
600     }
601 
602     /** {@inheritDoc} */
603     @Override
604     public final String toString()
605     {
606         return "DirectInfrastructurePerception";
607     }
608 
609     /**
610      * Helper class to return the distance over which a lane change is or is not possible. The distance is based on a
611      * LaneStructureRecord, and does not need an update as such.
612      * <p>
613      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
614      * <br>
615      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
616      * <p>
617      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 14 feb. 2018 <br>
618      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
619      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
620      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
621      */
622     private class LaneChangePossibility
623     {
624 
625         /** Structure the end of which determines the available distance. */
626         private final LaneStructureRecord record;
627 
628         /** Relative distance towards nose or tail. */
629         private final double dx;
630 
631         /** Whether to apply legal accessibility. */
632         private final boolean legal;
633 
634         /**
635          * @param record LaneStructureRecord; structure the end of which determines the available distance
636          * @param dx Length; relative distance towards nose or tail
637          * @param legal boolean; whether to apply legal accessibility
638          */
639         LaneChangePossibility(final LaneStructureRecord record, final Length dx, final boolean legal)
640         {
641             this.record = record;
642             this.dx = dx.si;
643             this.legal = legal;
644         }
645 
646         /**
647          * Returns the distance over which a lane change is (&gt;0) or is not (&lt;0) possible.
648          * @param lat LateralDirectionality; lateral direction
649          * @return Length distance over which a lane change is (&gt;0) or is not (&lt;0) possible
650          */
651         final Length getDistance(final LateralDirectionality lat)
652         {
653             double d = this.record.getStartDistance().si + this.record.getLane().getLength().si - this.dx;
654             if ((lat.isLeft() && this.record.possibleLeft(this.legal))
655                     || (lat.isRight() && this.record.possibleRight(this.legal)))
656             {
657                 return Length.createSI(d); // possible over d
658             }
659             return Length.createSI(-d); // not possible over d
660         }
661 
662     }
663 
664 }