View Javadoc
1   package org.opentrafficsim.road.network.sampling;
2   
3   import java.rmi.RemoteException;
4   import java.util.LinkedHashMap;
5   import java.util.LinkedHashSet;
6   import java.util.Map;
7   import java.util.Set;
8   
9   import org.djunits.unit.DurationUnit;
10  import org.djunits.value.vdouble.scalar.Acceleration;
11  import org.djunits.value.vdouble.scalar.Duration;
12  import org.djunits.value.vdouble.scalar.Frequency;
13  import org.djunits.value.vdouble.scalar.Length;
14  import org.djunits.value.vdouble.scalar.Speed;
15  import org.djunits.value.vdouble.scalar.Time;
16  import org.djutils.event.Event;
17  import org.djutils.event.EventListener;
18  import org.djutils.event.TimedEvent;
19  import org.djutils.event.reference.ReferenceType;
20  import org.djutils.exceptions.Throw;
21  import org.djutils.exceptions.Try;
22  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
23  import org.opentrafficsim.core.gtu.GtuException;
24  import org.opentrafficsim.core.gtu.RelativePosition;
25  import org.opentrafficsim.kpi.sampling.Sampler;
26  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
27  import org.opentrafficsim.kpi.sampling.filter.FilterDataType;
28  import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
29  import org.opentrafficsim.road.network.RoadNetwork;
30  import org.opentrafficsim.road.network.lane.CrossSectionLink;
31  import org.opentrafficsim.road.network.lane.Lane;
32  
33  import nl.tudelft.simulation.dsol.SimRuntimeException;
34  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
35  
36  /**
37   * Implementation of kpi sampler for OTS.
38   * <p>
39   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
40   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
41   * </p>
42   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
43   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
44   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
45   */
46  public class RoadSampler extends Sampler<GtuDataRoad, LaneDataRoad> implements EventListener
47  {
48  
49      /** */
50      private static final long serialVersionUID = 20200228L;
51  
52      /** Simulator. */
53      private final OtsSimulatorInterface simulator;
54  
55      /** Network. */
56      private final RoadNetwork network;
57  
58      /** Sampling interval. */
59      private final Duration samplingInterval;
60  
61      /** Registration of sampling events of each GTU per lane, if interval based. */
62      private final Map<String, Map<Lane, SimEventInterface<Duration>>> eventsPerGtu = new LinkedHashMap<>();
63  
64      /** Set of lanes the sampler knows each GTU to be at. Usually 1, could be 2 during a trajectory transition. */
65      private final Map<String, Set<Lane>> activeLanesPerGtu = new LinkedHashMap<>();
66  
67      /** Set of actively sampled GTUs. */
68      private final Set<String> activeGtus = new LinkedHashSet<>();
69  
70      /**
71       * Constructor which uses the operational plan updates of GTU's as sampling interval.
72       * @param network the network
73       * @throws NullPointerException if the simulator is {@code null}
74       */
75      public RoadSampler(final RoadNetwork network)
76      {
77          this(new LinkedHashSet<>(), new LinkedHashSet<>(), network);
78      }
79  
80      /**
81       * Constructor which uses the operational plan updates of GTU's as sampling interval.
82       * @param extendedDataTypes extended data types
83       * @param filterDataTypes filter data types
84       * @param network the network
85       * @throws NullPointerException if the simulator is {@code null}
86       */
87      public RoadSampler(final Set<ExtendedDataType<?, ?, ?, ? super GtuDataRoad>> extendedDataTypes,
88              final Set<FilterDataType<?, ? super GtuDataRoad>> filterDataTypes, final RoadNetwork network)
89      {
90          super(extendedDataTypes, filterDataTypes);
91          Throw.whenNull(network, "Network may not be null.");
92          this.network = network;
93          this.simulator = network.getSimulator();
94          this.samplingInterval = null;
95      }
96  
97      /**
98       * Constructor which uses the given frequency to determine the sampling interval.
99       * @param network the network
100      * @param frequency sampling frequency
101      * @throws NullPointerException if an input is {@code null}
102      * @throws IllegalArgumentException if frequency is negative or zero
103      */
104     public RoadSampler(final RoadNetwork network, final Frequency frequency)
105     {
106         this(new LinkedHashSet<>(), new LinkedHashSet<>(), network, frequency);
107     }
108 
109     /**
110      * Constructor which uses the given frequency to determine the sampling interval.
111      * @param extendedDataTypes extended data types
112      * @param filterDataTypes filter data types
113      * @param network the network
114      * @param frequency sampling frequency
115      * @throws NullPointerException if an input is {@code null}
116      * @throws IllegalArgumentException if frequency is negative or zero
117      */
118     public RoadSampler(final Set<ExtendedDataType<?, ?, ?, ? super GtuDataRoad>> extendedDataTypes,
119             final Set<FilterDataType<?, ? super GtuDataRoad>> filterDataTypes, final RoadNetwork network,
120             final Frequency frequency)
121     {
122         super(extendedDataTypes, filterDataTypes);
123         Throw.whenNull(network, "Network may not be null.");
124         Throw.whenNull(frequency, "Frequency may not be null.");
125         Throw.when(frequency.le(Frequency.ZERO), IllegalArgumentException.class,
126                 "Negative or zero sampling frequency is not permitted.");
127         this.network = network;
128         this.simulator = network.getSimulator();
129         this.samplingInterval = new Duration(1.0 / frequency.si, DurationUnit.SI);
130     }
131 
132     @Override
133     public final Time now()
134     {
135         return this.simulator.getSimulatorAbsTime();
136     }
137 
138     @Override
139     public final void scheduleStartRecording(final Time time, final LaneDataRoad lane)
140     {
141         try
142         {
143             this.simulator.scheduleEventAbsTime(time, this, "startRecording", new Object[] {lane});
144         }
145         catch (SimRuntimeException exception)
146         {
147             throw new RuntimeException("Cannot start recording.", exception);
148         }
149     }
150 
151     @Override
152     public final void scheduleStopRecording(final Time time, final LaneDataRoad lane)
153     {
154         try
155         {
156             this.simulator.scheduleEventAbsTime(time, this, "stopRecording", new Object[] {lane});
157         }
158         catch (SimRuntimeException exception)
159         {
160             throw new RuntimeException("Cannot stop recording.", exception);
161         }
162     }
163 
164     @Override
165     public final void initRecording(final LaneDataRoad lane)
166     {
167         Lane roadLane = lane.getLane();
168         // @docs/02-model-structure/djutils.md#event-producers-and-listeners
169         roadLane.addListener(this, Lane.GTU_ADD_EVENT, ReferenceType.WEAK);
170         roadLane.addListener(this, Lane.GTU_REMOVE_EVENT, ReferenceType.WEAK);
171         // @end
172         int count = 1;
173         for (LaneBasedGtu gtu : roadLane.getGtuList())
174         {
175             try
176             {
177                 // Payload: Object[] {String gtuId, Lane source}
178                 notify(new TimedEvent<>(Lane.GTU_ADD_EVENT,
179                         new Object[] {gtu.getId(), count, roadLane.getId(), roadLane.getLink().getId()},
180                         gtu.getSimulator().getSimulatorTime()));
181                 count++;
182             }
183             catch (Exception exception)
184             {
185                 throw new RuntimeException("Position cannot be obtained for GTU that is registered on a lane", exception);
186             }
187         }
188     }
189 
190     @Override
191     public final void finalizeRecording(final LaneDataRoad lane)
192     {
193         Lane roadLane = lane.getLane();
194         roadLane.removeListener(this, Lane.GTU_ADD_EVENT);
195         roadLane.removeListener(this, Lane.GTU_REMOVE_EVENT);
196     }
197 
198     @Override
199     // @docs/02-model-structure/djutils.md#event-producers-and-listeners (if-structure + add/removeListener(...))
200     public final void notify(final Event event) throws RemoteException
201     {
202         if (event.getType().equals(LaneBasedGtu.LANEBASED_MOVE_EVENT))
203         {
204             // Payload: [String gtuId, PositionVector currentPosition, Direction currentDirection, Speed speed, Acceleration
205             // acceleration, TurnIndicatorStatus turnIndicatorStatus, Length odometer, Link id of referenceLane, Lane id of
206             // referenceLane, Length positionOnReferenceLane]
207             Object[] payload = (Object[]) event.getContent();
208             CrossSectionLink link = (CrossSectionLink) this.network.getLink(payload[7].toString());
209             Lane lane = (Lane) link.getCrossSectionElement(payload[8].toString());
210             LaneBasedGtu gtu = (LaneBasedGtu) this.network.getGTU(payload[0].toString());
211             LaneDataRoad laneData = new LaneDataRoad(lane);
212 
213             if (!this.activeGtus.contains(gtu.getId()) && this.getSamplerData().contains(laneData))
214             {
215                 // GTU add was skipped during add event due to an improper phase of initialization, do here instead
216                 addGtu(laneData, new GtuDataRoad(gtu));
217                 this.activeGtus.add(gtu.getId());
218             }
219             snapshot(laneData, (Length) payload[9], (Speed) payload[3], (Acceleration) payload[4], now(), new GtuDataRoad(gtu));
220         }
221         else if (event.getType().equals(Lane.GTU_ADD_EVENT))
222         {
223             // Payload: Object[] {String gtuId, int count_after_addition, String laneId, String linkId}
224             Object[] payload = (Object[]) event.getContent();
225             Lane lane = (Lane) ((CrossSectionLink) this.network.getLink((String) payload[3]))
226                     .getCrossSectionElement((String) payload[2]);
227             LaneDataRoad laneData = new LaneDataRoad(lane);
228             if (!getSamplerData().contains(laneData))
229             {
230                 return; // we are not sampling this Lane
231             }
232             LaneBasedGtu gtu = (LaneBasedGtu) this.network.getGTU((String) payload[0]);
233 
234             // Skip add when first encountering this GTU, it is in an improper phase of initialization.
235             // If interval-based, a GTU is also not in the active list in the moment of an instantaneous lane-change.
236             boolean active = this.activeGtus.contains(gtu.getId());
237             if (active)
238             {
239                 Length position = Try.assign(() -> gtu.position(lane, RelativePosition.REFERENCE_POSITION),
240                         "Could not determine position.");
241                 Speed speed = gtu.getSpeed();
242                 Acceleration acceleration = gtu.getAcceleration();
243                 addGtuWithSnapshot(laneData, position, speed, acceleration, now(), new GtuDataRoad(gtu));
244             }
245 
246             if (isIntervalBased())
247             {
248                 double currentTime = now().getSI();
249                 double next = Math.ceil(currentTime / this.samplingInterval.getSI()) * this.samplingInterval.getSI();
250                 if (next > currentTime)
251                 {
252                     // add sample at current time, then synchronize in interval
253                     notifySample(gtu, lane, false);
254                     scheduleSamplingInterval(gtu, lane, Duration.instantiateSI(next - currentTime));
255                 }
256                 else
257                 {
258                     // already synchronous
259                     notifySample(gtu, lane, true);
260                 }
261             }
262             else
263             {
264                 this.activeLanesPerGtu.computeIfAbsent(gtu.getId(), (key) -> new LinkedHashSet<>()).add(lane);
265                 gtu.addListener(this, LaneBasedGtu.LANEBASED_MOVE_EVENT, ReferenceType.WEAK);
266             }
267         }
268         else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
269         {
270             // Payload: Object[] {String gtuId, LaneBasedGtu gtu, int count_after_removal, Length position, String laneId,
271             // String linkId}
272             Object[] payload = (Object[]) event.getContent();
273             Lane lane = (Lane) ((CrossSectionLink) this.network.getLink((String) payload[5]))
274                     .getCrossSectionElement((String) payload[4]);
275             LaneDataRoad laneData = new LaneDataRoad(lane);
276             LaneBasedGtu gtu = (LaneBasedGtu) payload[1];
277             Length position = (Length) payload[3];
278             Speed speed = gtu.getSpeed();
279             Acceleration acceleration = gtu.getAcceleration();
280 
281             removeGtuWithSnapshot(laneData, position, speed, acceleration, now(), new GtuDataRoad(gtu));
282 
283             if (isIntervalBased())
284             {
285                 Map<Lane, SimEventInterface<Duration>> events = this.eventsPerGtu.get(gtu.getId());
286                 SimEventInterface<Duration> e = events.remove(lane);
287                 if (e != null)
288                 {
289                     this.simulator.cancelEvent(e);
290                 }
291                 if (events.isEmpty())
292                 {
293                     this.eventsPerGtu.remove(gtu.getId());
294                     this.activeGtus.remove(gtu.getId());
295                 }
296             }
297             else
298             {
299                 this.activeLanesPerGtu.get(gtu.getId()).remove(lane);
300                 if (this.activeLanesPerGtu.get(gtu.getId()).isEmpty())
301                 {
302                     this.activeLanesPerGtu.remove(gtu.getId());
303                     gtu.removeListener(this, LaneBasedGtu.LANEBASED_MOVE_EVENT);
304                     this.activeGtus.remove(gtu.getId());
305                 }
306             }
307         }
308     }
309 
310     /**
311      * @return whether sampling is interval based
312      */
313     private boolean isIntervalBased()
314     {
315         return this.samplingInterval != null;
316     }
317 
318     /**
319      * Schedules a sampling event for the given gtu on the given lane for the sampling interval from the current time.
320      * @param gtu gtu to sample
321      * @param lane lane where the gtu is at
322      * @param inTime relative time to schedule
323      */
324     private void scheduleSamplingInterval(final LaneBasedGtu gtu, final Lane lane, final Duration inTime)
325     {
326         SimEventInterface<Duration> simEvent;
327         try
328         {
329             simEvent = this.simulator.scheduleEventRel(inTime, this, "notifySample", new Object[] {gtu, lane, true});
330         }
331         catch (SimRuntimeException exception)
332         {
333             // should not happen with getSimulatorTime.add()
334             throw new RuntimeException("Scheduling sampling in the past.", exception);
335         }
336         this.eventsPerGtu.computeIfAbsent(gtu.getId(), (key) -> new LinkedHashMap<>()).put(lane, simEvent);
337     }
338 
339     /**
340      * Samples a gtu and schedules the next sampling event. This is used for interval-based sampling.
341      * @param gtu gtu to sample
342      * @param lane lane direction where the gtu is at
343      * @param scheduleNext whether to schedule the next event
344      */
345     public final void notifySample(final LaneBasedGtu gtu, final Lane lane, final boolean scheduleNext)
346     {
347         LaneDataRoad laneData = new LaneDataRoad(lane);
348         try
349         {
350             Length position = gtu.position(lane, RelativePosition.REFERENCE_POSITION);
351             if (this.activeGtus.contains(gtu.getId()))
352             {
353                 // already recording this GTU, just trigger a record through a move
354                 snapshot(laneData, position, gtu.getSpeed(), gtu.getAcceleration(), now(), new GtuDataRoad(gtu));
355             }
356             else
357             {
358                 // first time encountering this GTU so add, which also triggers a record through a move
359                 addGtuWithSnapshot(laneData, position, gtu.getSpeed(), gtu.getAcceleration(), now(), new GtuDataRoad(gtu));
360                 this.activeGtus.add(gtu.getId());
361             }
362         }
363         catch (GtuException exception)
364         {
365             throw new RuntimeException("Requesting position on lane, but the GTU is not on the lane.", exception);
366         }
367         if (scheduleNext)
368         {
369             scheduleSamplingInterval(gtu, lane, this.samplingInterval);
370         }
371     }
372 
373     @Override
374     public final int hashCode()
375     {
376         final int prime = 31;
377         int result = super.hashCode();
378         result = prime * result + ((this.eventsPerGtu == null) ? 0 : this.eventsPerGtu.hashCode());
379         result = prime * result + ((this.samplingInterval == null) ? 0 : this.samplingInterval.hashCode());
380         result = prime * result + ((this.simulator == null) ? 0 : this.simulator.hashCode());
381         return result;
382     }
383 
384     @Override
385     public final boolean equals(final Object obj)
386     {
387         if (this == obj)
388         {
389             return true;
390         }
391         if (!super.equals(obj))
392         {
393             return false;
394         }
395         if (getClass() != obj.getClass())
396         {
397             return false;
398         }
399         RoadSampler other = (RoadSampler) obj;
400         if (this.eventsPerGtu == null)
401         {
402             if (other.eventsPerGtu != null)
403             {
404                 return false;
405             }
406         }
407         else if (!this.eventsPerGtu.equals(other.eventsPerGtu))
408         {
409             return false;
410         }
411         if (this.samplingInterval == null)
412         {
413             if (other.samplingInterval != null)
414             {
415                 return false;
416             }
417         }
418         else if (!this.samplingInterval.equals(other.samplingInterval))
419         {
420             return false;
421         }
422         if (this.simulator == null)
423         {
424             if (other.simulator != null)
425             {
426                 return false;
427             }
428         }
429         else if (!this.simulator.equals(other.simulator))
430         {
431             return false;
432         }
433         return true;
434     }
435 
436     @Override
437     public final String toString()
438     {
439         return "RoadSampler [samplingInterval=" + this.samplingInterval + "]";
440         // do not use "this.eventPerGtu", it creates circular toString and hence stack overflow
441     }
442 
443     /**
444      * Returns a factory to create a sampler.
445      * @param network network
446      * @return factory to create a sampler
447      */
448     public static Factory build(final RoadNetwork network)
449     {
450         return new Factory(network);
451     }
452 
453     /** Factory for {@code RoadSampler}. */
454     public static final class Factory
455     {
456 
457         /** Simulator. */
458         private final RoadNetwork network;
459 
460         /** Registration of included extended data types. */
461         private final Set<ExtendedDataType<?, ?, ?, ? super GtuDataRoad>> extendedDataTypes = new LinkedHashSet<>();
462 
463         /** Set of registered filter data types. */
464         private final Set<FilterDataType<?, ? super GtuDataRoad>> filterDataTypes = new LinkedHashSet<>();
465 
466         /** Frequency. */
467         private Frequency freq;
468 
469         /**
470          * Constructor.
471          * @param network network
472          */
473         Factory(final RoadNetwork network)
474         {
475             this.network = network;
476         }
477 
478         /**
479          * Register extended data type.
480          * @param extendedDataType extended data type
481          * @return this factory
482          */
483         public Factory registerExtendedDataType(final ExtendedDataType<?, ?, ?, ? super GtuDataRoad> extendedDataType)
484         {
485             Throw.whenNull(extendedDataType, "Extended data type may not be null.");
486             this.extendedDataTypes.add(extendedDataType);
487             return this;
488         }
489 
490         /**
491          * Register filter data type.
492          * @param filterDataType filter data type
493          * @return this factory
494          */
495         public Factory registerFilterDataType(final FilterDataType<?, ? super GtuDataRoad> filterDataType)
496         {
497             Throw.whenNull(filterDataType, "Filter data type may not be null.");
498             this.filterDataTypes.add(filterDataType);
499             return this;
500         }
501 
502         /**
503          * Sets the frequency. If no frequency is set, a sampler is created that records on move events of GTU's.
504          * @param frequency frequency
505          * @return this factory
506          */
507         public Factory setFrequency(final Frequency frequency)
508         {
509             this.freq = frequency;
510             return this;
511         }
512 
513         /**
514          * Create sampler.
515          * @return sampler
516          */
517         public RoadSampler create()
518         {
519             return this.freq == null ? new RoadSampler(this.extendedDataTypes, this.filterDataTypes, this.network)
520                     : new RoadSampler(this.extendedDataTypes, this.filterDataTypes, this.network, this.freq);
521         }
522 
523     }
524 
525 }