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