View Javadoc
1   package org.opentrafficsim.kpi.sampling;
2   
3   import java.io.BufferedWriter;
4   import java.io.IOException;
5   import java.util.ArrayList;
6   import java.util.HashMap;
7   import java.util.LinkedHashMap;
8   import java.util.LinkedHashSet;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Set;
12  
13  import org.djunits.value.vdouble.scalar.Acceleration;
14  import org.djunits.value.vdouble.scalar.Length;
15  import org.djunits.value.vdouble.scalar.Speed;
16  import org.djunits.value.vdouble.scalar.Time;
17  import org.opentrafficsim.base.CompressedFileWriter;
18  import org.opentrafficsim.kpi.interfaces.GtuDataInterface;
19  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
20  import org.opentrafficsim.kpi.sampling.meta.MetaData;
21  import org.opentrafficsim.kpi.sampling.meta.MetaDataSet;
22  import org.opentrafficsim.kpi.sampling.meta.MetaDataType;
23  
24  import nl.tudelft.simulation.language.Throw;
25  
26  /**
27   * Sampler is the highest level organizer for sampling.
28   * <p>
29   * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
30   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
31   * <p>
32   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Sep 22, 2016 <br>
33   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
34   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
35   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
36   * @param <G> gtu data type
37   */
38  public abstract class Sampler<G extends GtuDataInterface>
39  {
40  
41      /** Map with all sampling data. */
42      private final Map<KpiLaneDirection, TrajectoryGroup> trajectories = new LinkedHashMap<>();
43  
44      /** End times of active samplings. */
45      private final Map<KpiLaneDirection, Time> endTimes = new LinkedHashMap<>();
46  
47      /** Registration of current trajectories of each GTU per lane. */
48      private final Map<String, Map<KpiLaneDirection, Trajectory<G>>> trajectoryPerGtu = new LinkedHashMap<>();
49  
50      /** Registration of included extended data types. */
51      private final Set<ExtendedDataType<?, ?, ?, G>> extendedDataTypes = new LinkedHashSet<>();
52  
53      /** Set of registered meta data types. */
54      private Set<MetaDataType<?>> registeredMetaDataTypes = new LinkedHashSet<>();
55  
56      /** Space time regions. */
57      private Set<SpaceTimeRegion> spaceTimeRegions = new LinkedHashSet<>();
58  
59      /**
60       * @param spaceTimeRegion space-time region
61       * @throws IllegalStateException if data is not available from the requested start time
62       */
63      public final void registerSpaceTimeRegion(final SpaceTimeRegion spaceTimeRegion)
64      {
65          Throw.whenNull(spaceTimeRegion, "SpaceTimeRegion may not be null.");
66          Time firstPossibleDataTime;
67          if (this.trajectories.containsKey(spaceTimeRegion.getLaneDirection()))
68          {
69              firstPossibleDataTime = this.trajectories.get(spaceTimeRegion.getLaneDirection()).getStartTime();
70          }
71          else
72          {
73              firstPossibleDataTime = now();
74          }
75          Throw.when(spaceTimeRegion.getStartTime().lt(firstPossibleDataTime), IllegalStateException.class,
76                  "Space time region with start time %s is defined while data is available from %s onwards.",
77                  spaceTimeRegion.getStartTime(), firstPossibleDataTime);
78          if (this.trajectories.containsKey(spaceTimeRegion.getLaneDirection()))
79          {
80              this.endTimes.put(spaceTimeRegion.getLaneDirection(),
81                      Time.max(this.endTimes.get(spaceTimeRegion.getLaneDirection()), spaceTimeRegion.getEndTime()));
82          }
83          else
84          {
85              this.endTimes.put(spaceTimeRegion.getLaneDirection(), spaceTimeRegion.getEndTime());
86              scheduleStartRecording(spaceTimeRegion.getStartTime(), spaceTimeRegion.getLaneDirection());
87          }
88          scheduleStopRecording(this.endTimes.get(spaceTimeRegion.getLaneDirection()), spaceTimeRegion.getLaneDirection());
89          this.spaceTimeRegions.add(spaceTimeRegion);
90      }
91  
92      /**
93       * Returns the current simulation time.
94       * @return current simulation time
95       */
96      public abstract Time now();
97  
98      /**
99       * Schedules the start of recording for a given lane-direction.
100      * @param time time to start recording
101      * @param kpiLaneDirection lane-direction to start recording
102      */
103     public abstract void scheduleStartRecording(final Time time, final KpiLaneDirection kpiLaneDirection);
104 
105     /**
106      * Schedules the stop of recording for a given lane-direction.
107      * @param time time to stop recording
108      * @param kpiLaneDirection lane-direction to stop recording
109      */
110     public abstract void scheduleStopRecording(final Time time, final KpiLaneDirection kpiLaneDirection);
111 
112     /**
113      * Registers meta data types that will be stored with the trajectories.
114      * @param metaDataTypes meta data types to register
115      */
116     public final void registerMetaDataTypes(final Set<MetaDataType<?>> metaDataTypes)
117     {
118         Throw.whenNull(metaDataTypes, "MetaDataTypes may not be null.");
119         this.registeredMetaDataTypes.addAll(metaDataTypes);
120     }
121 
122     /**
123      * Registers extended data type that will be stored with the trajectories.
124      * @param extendedDataType extended data type to register
125      */
126     public final void registerExtendedDataType(final ExtendedDataType<?, ?, ?, G> extendedDataType)
127     {
128         Throw.whenNull(extendedDataType, "ExtendedDataType may not be null.");
129         this.extendedDataTypes.add(extendedDataType);
130     }
131 
132     /**
133      * Whether this sampler has the given extended data type registered to it.
134      * @param extendedDataType extended data type
135      * @return whether this sampler has the given extended data type registered to it
136      */
137     public boolean contains(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
138     {
139         return this.extendedDataTypes.contains(extendedDataType);
140     }
141 
142     /**
143      * Start recording at the given time (which should be the current time) on the given lane direction.
144      * @param kpiLaneDirection lane direction
145      */
146     public final void startRecording(final KpiLaneDirection kpiLaneDirection)
147     {
148         Throw.whenNull(kpiLaneDirection, "KpiLaneDirection may not be null.");
149         if (this.trajectories.containsKey(kpiLaneDirection))
150         {
151             return;
152         }
153         this.trajectories.put(kpiLaneDirection, new TrajectoryGroup(now(), kpiLaneDirection));
154         initRecording(kpiLaneDirection);
155     }
156 
157     /**
158      * Adds listeners to start recording.
159      * @param kpiLaneDirection lane direction to initialize recording for
160      */
161     public abstract void initRecording(final KpiLaneDirection kpiLaneDirection);
162 
163     /**
164      * Stop recording at given lane direction.
165      * @param kpiLaneDirection lane direction
166      */
167     public final void stopRecording(final KpiLaneDirection kpiLaneDirection)
168     {
169         Throw.whenNull(kpiLaneDirection, "KpiLaneDirection may not be null.");
170         if (!this.trajectories.containsKey(kpiLaneDirection) || this.endTimes.get(kpiLaneDirection).gt(now()))
171         {
172             return;
173         }
174         finalizeRecording(kpiLaneDirection);
175     }
176 
177     /**
178      * Remove listeners to stop recording.
179      * @param kpiLaneDirection lane direction to finalize recording for
180      */
181     public abstract void finalizeRecording(final KpiLaneDirection kpiLaneDirection);
182 
183     /**
184      * Creates a trajectory with the current snapshot of a GTU.
185      * @param kpiLaneDirection lane direction the gtu is at
186      * @param position position of the gtu on the lane
187      * @param speed speed of the gtu
188      * @param acceleration acceleration of the gtu
189      * @param time current time
190      * @param gtu gtu
191      */
192     public final void processGtuAddEvent(final KpiLaneDirection kpiLaneDirection, final Length position, final Speed speed,
193             final Acceleration acceleration, final Time time, final G gtu)
194     {
195         Throw.whenNull(kpiLaneDirection, "KpiLaneDirection may not be null.");
196         Throw.whenNull(position, "Position may not be null.");
197         Throw.whenNull(speed, "Speed may not be null.");
198         Throw.whenNull(acceleration, "Acceleration may not be null.");
199         Throw.whenNull(time, "Time may not be null.");
200         Throw.whenNull(gtu, "GtuDataInterface may not be null.");
201         if (kpiLaneDirection.getLaneData().getLength().lt(position))
202         {
203             // ignore event if beyond lane length (may happen during lane change)
204             return;
205         }
206         String gtuId = gtu.getId();
207         Trajectory<G> trajectory = new Trajectory<>(gtu, makeMetaData(gtu), this.extendedDataTypes, kpiLaneDirection);
208         if (!this.trajectoryPerGtu.containsKey(gtuId))
209         {
210             Map<KpiLaneDirection, Trajectory<G>> map = new LinkedHashMap<>();
211             this.trajectoryPerGtu.put(gtuId, map);
212         }
213         this.trajectoryPerGtu.get(gtuId).put(kpiLaneDirection, trajectory);
214         this.trajectories.get(kpiLaneDirection).addTrajectory(trajectory);
215         processGtuMoveEvent(kpiLaneDirection, position, speed, acceleration, time, gtu);
216     }
217 
218     /**
219      * Adds a new snapshot of a GTU to its recording trajectory, if recorded. This method may be invoked on GTU that are not
220      * being recorded; the event will then be ignored.
221      * @param kpiLaneDirection lane direction the gtu is at
222      * @param position position of the gtu on the lane
223      * @param speed speed of the gtu
224      * @param acceleration acceleration of the gtu
225      * @param time current time
226      * @param gtu gtu
227      */
228     public final void processGtuMoveEvent(final KpiLaneDirection kpiLaneDirection, final Length position, final Speed speed,
229             final Acceleration acceleration, final Time time, final G gtu)
230     {
231         Throw.whenNull(kpiLaneDirection, "KpiLaneDirection may not be null.");
232         Throw.whenNull(position, "Position may not be null.");
233         Throw.whenNull(speed, "Speed may not be null.");
234         Throw.whenNull(acceleration, "Acceleration may not be null.");
235         Throw.whenNull(time, "Time may not be null.");
236         Throw.whenNull(gtu, "GtuDataInterface may not be null.");
237         String gtuId = gtu.getId();
238         if (this.trajectoryPerGtu.containsKey(gtuId) && this.trajectoryPerGtu.get(gtuId).containsKey(kpiLaneDirection))
239         {
240             this.trajectoryPerGtu.get(gtuId).get(kpiLaneDirection).add(position, speed, acceleration, time, gtu);
241         }
242     }
243 
244     /**
245      * Finalizes a trajectory with the current snapshot of a GTU.
246      * @param kpiLaneDirection lane direction the gtu is at
247      * @param position position of the gtu on the lane
248      * @param speed speed of the gtu
249      * @param acceleration acceleration of the gtu
250      * @param time current time
251      * @param gtu gtu
252      */
253     public final void processGtuRemoveEvent(final KpiLaneDirection kpiLaneDirection, final Length position, final Speed speed,
254             final Acceleration acceleration, final Time time, final G gtu)
255     {
256         processGtuMoveEvent(kpiLaneDirection, position, speed, acceleration, time, gtu);
257         processGtuRemoveEvent(kpiLaneDirection, gtu);
258     }
259 
260     /**
261      * Finalizes a trajectory.
262      * @param kpiLaneDirection lane direction the gtu is at
263      * @param gtu gtu
264      */
265     public final void processGtuRemoveEvent(final KpiLaneDirection kpiLaneDirection, final G gtu)
266     {
267         Throw.whenNull(kpiLaneDirection, "KpiLaneDirection may not be null.");
268         Throw.whenNull(gtu, "GtuDataInterface may not be null.");
269         String gtuId = gtu.getId();
270         if (this.trajectoryPerGtu.containsKey(gtuId))
271         {
272             this.trajectoryPerGtu.get(gtuId).remove(kpiLaneDirection);
273             if (this.trajectoryPerGtu.get(gtuId).isEmpty())
274             {
275                 this.trajectoryPerGtu.remove(gtuId);
276             }
277         }
278     }
279 
280     /**
281      * @param gtu gtu to return meta data for
282      * @param <T> underlying type of a meta data type
283      * @return meta data for the given gtu
284      */
285     @SuppressWarnings("unchecked")
286     private <T> MetaData makeMetaData(final G gtu)
287     {
288         MetaData metaData = new MetaData();
289         for (MetaDataType<?> metaDataType : this.registeredMetaDataTypes)
290         {
291             T value = (T) metaDataType.getValue(gtu);
292             if (value != null)
293             {
294                 metaData.put((MetaDataType<T>) metaDataType, value);
295             }
296         }
297         return metaData;
298     }
299 
300     /**
301      * Returns whether there is data for the give lane direction.
302      * @param kpiLaneDirection lane direction
303      * @return whether there is data for the give lane direction
304      */
305     public final boolean contains(final KpiLaneDirection kpiLaneDirection)
306     {
307         return this.trajectories.containsKey(kpiLaneDirection);
308     }
309 
310     /**
311      * Returns the trajectory group of given lane direction.
312      * @param kpiLaneDirection lane direction
313      * @return trajectory group of given lane direction, {@code null} if none
314      */
315     public final TrajectoryGroup getTrajectoryGroup(final KpiLaneDirection kpiLaneDirection)
316     {
317         return this.trajectories.get(kpiLaneDirection);
318     }
319 
320     /**
321      * Write the contents of the sampler in to a file. By default this is zipped and numeric data is formated %.3f.
322      * @param file file
323      */
324     public final void writeToFile(final String file)
325     {
326         writeToFile(file, "%.3f", CompressionMethod.ZIP);
327     }
328 
329     /**
330      * Write the contents of the sampler in to a file.
331      * @param file file
332      * @param format number format, as used in {@code String.format()}
333      * @param compression how to compress the data
334      */
335     public final void writeToFile(final String file, final String format, final CompressionMethod compression)
336     {
337         int counter = 0;
338         BufferedWriter bw = CompressedFileWriter.create(file, compression.equals(CompressionMethod.ZIP));
339         // create Query, as this class is designed to filter for space-time regions
340         Query query = new Query(this, "", new MetaDataSet());
341         for (SpaceTimeRegion str : this.spaceTimeRegions)
342         {
343             query.addSpaceTimeRegion(str.getLaneDirection(), str.getStartPosition(), str.getEndPosition(), str.getStartTime(),
344                     str.getEndTime());
345         }
346         List<TrajectoryGroup> groups = query.getTrajectoryGroups(Time.createSI(Double.POSITIVE_INFINITY));
347         try
348         {
349             // gather all meta data types for the header line
350             List<MetaDataType<?>> allMetaDataTypes = new ArrayList<>();
351             for (TrajectoryGroup group : groups)
352             {
353                 for (Trajectory<?> trajectory : group.getTrajectories())
354                 {
355                     for (MetaDataType<?> metaDataType : trajectory.getMetaDataTypes())
356                     {
357                         if (!allMetaDataTypes.contains(metaDataType))
358                         {
359                             allMetaDataTypes.add(metaDataType);
360                         }
361                     }
362                 }
363             }
364             // gather all extended data types for the header line
365             List<ExtendedDataType<?, ?, ?, ?>> allExtendedDataTypes = new ArrayList<>();
366             for (TrajectoryGroup group : groups)
367             {
368                 for (Trajectory<?> trajectory : group.getTrajectories())
369                 {
370                     for (ExtendedDataType<?, ?, ?, ?> extendedDataType : trajectory.getExtendedDataTypes())
371                     {
372                         if (!allExtendedDataTypes.contains(extendedDataType))
373                         {
374                             allExtendedDataTypes.add(extendedDataType);
375                         }
376                     }
377                 }
378             }
379             // create header line
380             StringBuilder str = new StringBuilder();
381             str.append("traj#,linkId,laneId&dir,gtuId,t,x,v,a");
382             for (MetaDataType<?> metaDataType : allMetaDataTypes)
383             {
384                 str.append(",");
385                 str.append(metaDataType.getId());
386             }
387             for (ExtendedDataType<?, ?, ?, ?> extendedDataType : allExtendedDataTypes)
388             {
389                 str.append(",");
390                 str.append(extendedDataType.getId());
391             }
392             bw.write(str.toString());
393             bw.newLine();
394             for (TrajectoryGroup group : groups)
395             {
396                 for (Trajectory<?> trajectory : group.getTrajectories())
397                 {
398                     counter++;
399                     float[] t = trajectory.getT();
400                     float[] x = trajectory.getX();
401                     float[] v = trajectory.getV();
402                     float[] a = trajectory.getA();
403                     Map<ExtendedDataType<?, ?, ?, ?>, Object> extendedData = new HashMap<>();
404                     for (ExtendedDataType<?, ?, ?, ?> extendedDataType : allExtendedDataTypes)
405                     {
406                         if (trajectory.contains(extendedDataType))
407                         {
408                             try
409                             {
410                                 extendedData.put(extendedDataType, trajectory.getExtendedData(extendedDataType));
411                             }
412                             catch (SamplingException exception)
413                             {
414                                 // should not occur, we obtain the extended data types from the trajectory
415                                 throw new RuntimeException("Error while loading extended data type.", exception);
416                             }
417                         }
418                     }
419                     for (int i = 0; i < t.length; i++)
420                     {
421                         str = new StringBuilder();
422                         str.append(counter);
423                         str.append(",");
424                         if (!compression.equals(CompressionMethod.OMIT_DUPLICATE_INFO) || i == 0)
425                         {
426                             str.append(group.getLaneDirection().getLaneData().getLinkData().getId());
427                             str.append(",");
428                             str.append(group.getLaneDirection().getLaneData().getId());
429                             str.append(group.getLaneDirection().getKpiDirection().isPlus() ? "+" : "-");
430                             str.append(",");
431                             str.append(trajectory.getGtuId());
432                             str.append(",");
433                         }
434                         else
435                         {
436                             // one trajectory is on the same lane and pertains to the same GTU, no need to repeat data
437                             str.append(",,,");
438                         }
439                         str.append(String.format(format, t[i]));
440                         str.append(",");
441                         str.append(String.format(format, x[i]));
442                         str.append(",");
443                         str.append(String.format(format, v[i]));
444                         str.append(",");
445                         str.append(String.format(format, a[i]));
446                         for (MetaDataType<?> metaDataType : allMetaDataTypes)
447                         {
448                             str.append(",");
449                             if (i == 0 && trajectory.contains(metaDataType))
450                             {
451                                 // no need to repeat meta data
452                                 str.append(metaDataType.formatValue(format, castValue(trajectory.getMetaData(metaDataType))));
453                             }
454                         }
455                         for (ExtendedDataType<?, ?, ?, ?> extendedDataType : allExtendedDataTypes)
456                         {
457                             str.append(",");
458                             if (trajectory.contains(extendedDataType))
459                             {
460                                 try
461                                 {
462                                     str.append(
463                                             extendedDataType.formatValue(format, castValue(extendedData, extendedDataType, i)));
464                                 }
465                                 catch (SamplingException exception)
466                                 {
467                                     // should not occur, we obtain the extended data types from the trajectory
468                                     throw new RuntimeException("Error while loading extended data type.", exception);
469                                 }
470                             }
471                         }
472                         bw.write(str.toString());
473                         bw.newLine();
474                     }
475                 }
476             }
477         }
478         catch (IOException exception)
479         {
480             throw new RuntimeException("Could not write to file.", exception);
481         }
482         // close file on fail
483         finally
484         {
485             try
486             {
487                 if (bw != null)
488                 {
489                     bw.close();
490                 }
491             }
492             catch (IOException ex)
493             {
494                 ex.printStackTrace();
495             }
496         }
497     }
498 
499     /**
500      * Cast value to type for meta data.
501      * @param value value object to cast
502      * @return cast value
503      */
504     @SuppressWarnings("unchecked")
505     private <T> T castValue(final Object value)
506     {
507         return (T) value;
508     }
509 
510     /**
511      * Cast value to type for extended data.
512      * @param extendedData extended data of trajectory in output form
513      * @param extendedDataType extended data type
514      * @param i index of value to return
515      * @return cast value
516      * @throws SamplingException when the found index is out of bounds
517      */
518     @SuppressWarnings("unchecked")
519     private <T, O, S> T castValue(final Map<ExtendedDataType<?, ?, ?, ?>, Object> extendedData,
520             final ExtendedDataType<?, ?, ?, ?> extendedDataType, final int i) throws SamplingException
521     {
522         // is only called on value directly taken from an ExtendedDataType within range of trajectory
523         ExtendedDataType<T, O, S, ?> edt = (ExtendedDataType<T, O, S, ?>) extendedDataType;
524         return edt.getOutputValue((O) extendedData.get(edt), i);
525     }
526 
527     /** {@inheritDoc} */
528     @Override
529     public int hashCode()
530     {
531         final int prime = 31;
532         int result = 1;
533         result = prime * result + ((this.endTimes == null) ? 0 : this.endTimes.hashCode());
534         result = prime * result + ((this.extendedDataTypes == null) ? 0 : this.extendedDataTypes.hashCode());
535         result = prime * result + ((this.registeredMetaDataTypes == null) ? 0 : this.registeredMetaDataTypes.hashCode());
536         result = prime * result + ((this.trajectories == null) ? 0 : this.trajectories.hashCode());
537         result = prime * result + ((this.trajectoryPerGtu == null) ? 0 : this.trajectoryPerGtu.hashCode());
538         return result;
539     }
540 
541     /** {@inheritDoc} */
542     @Override
543     public boolean equals(final Object obj)
544     {
545         if (this == obj)
546         {
547             return true;
548         }
549         if (obj == null)
550         {
551             return false;
552         }
553         if (getClass() != obj.getClass())
554         {
555             return false;
556         }
557         Sampler<?> other = (Sampler<?>) obj;
558         if (this.endTimes == null)
559         {
560             if (other.endTimes != null)
561             {
562                 return false;
563             }
564         }
565         else if (!this.endTimes.equals(other.endTimes))
566         {
567             return false;
568         }
569         if (this.extendedDataTypes == null)
570         {
571             if (other.extendedDataTypes != null)
572             {
573                 return false;
574             }
575         }
576         else if (!this.extendedDataTypes.equals(other.extendedDataTypes))
577         {
578             return false;
579         }
580         if (this.registeredMetaDataTypes == null)
581         {
582             if (other.registeredMetaDataTypes != null)
583             {
584                 return false;
585             }
586         }
587         else if (!this.registeredMetaDataTypes.equals(other.registeredMetaDataTypes))
588         {
589             return false;
590         }
591         if (this.trajectories == null)
592         {
593             if (other.trajectories != null)
594             {
595                 return false;
596             }
597         }
598         else if (!this.trajectories.equals(other.trajectories))
599         {
600             return false;
601         }
602         if (this.trajectoryPerGtu == null)
603         {
604             if (other.trajectoryPerGtu != null)
605             {
606                 return false;
607             }
608         }
609         else if (!this.trajectoryPerGtu.equals(other.trajectoryPerGtu))
610         {
611             return false;
612         }
613         return true;
614     }
615 
616     /**
617      * Defines the compression method for stored data.
618      * <p>
619      * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
620      * <br>
621      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
622      * <p>
623      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 3 mei 2017 <br>
624      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
625      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
626      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
627      */
628     public enum CompressionMethod
629     {
630         /** No compression. */
631         NONE,
632 
633         /** Duplicate info per trajectory is only stored at the first sample, and empty for other samples. */
634         OMIT_DUPLICATE_INFO,
635 
636         /** Zip compression. */
637         ZIP,
638 
639     }
640 
641 }