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