View Javadoc
1   package org.opentrafficsim.kpi.sampling;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.lang.reflect.Field;
6   import java.util.ArrayList;
7   import java.util.Collection;
8   import java.util.Collections;
9   import java.util.ConcurrentModificationException;
10  import java.util.Iterator;
11  import java.util.LinkedHashMap;
12  import java.util.LinkedHashSet;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Map.Entry;
16  import java.util.NoSuchElementException;
17  import java.util.Set;
18  import java.util.stream.IntStream;
19  
20  import org.djunits.unit.AccelerationUnit;
21  import org.djunits.unit.DurationUnit;
22  import org.djunits.unit.LengthUnit;
23  import org.djunits.unit.SpeedUnit;
24  import org.djunits.unit.Unit;
25  import org.djunits.value.base.Scalar;
26  import org.djunits.value.vfloat.scalar.FloatAcceleration;
27  import org.djunits.value.vfloat.scalar.FloatDuration;
28  import org.djunits.value.vfloat.scalar.FloatLength;
29  import org.djunits.value.vfloat.scalar.FloatSpeed;
30  import org.djutils.data.Column;
31  import org.djutils.data.Row;
32  import org.djutils.data.Table;
33  import org.djutils.data.csv.CsvData;
34  import org.djutils.data.serialization.TextSerializationException;
35  import org.djutils.exceptions.Throw;
36  import org.djutils.io.CompressedFileWriter;
37  import org.opentrafficsim.kpi.interfaces.GtuData;
38  import org.opentrafficsim.kpi.interfaces.LaneData;
39  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
40  import org.opentrafficsim.kpi.sampling.meta.FilterDataType;
41  
42  /**
43   * SamplerData is a storage for trajectory data. Adding trajectory groups can only be done by subclasses. This is however not a
44   * guaranteed read-only class. Any type can obtain the lane directions and with those the coupled trajectory groups.
45   * Trajectories can be added to these trajectory groups. Data can also be added to the trajectories themselves.
46   * <p>
47   * Copyright (c) 2020-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
48   * BSD-style license. See <a href="https://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
49   * </p>
50   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
51   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
52   * @author <a href="https://www.transport.citg.tudelft.nl">Wouter Schakel</a>
53   * @param <G> gtu data type
54   */
55  public class SamplerData<G extends GtuData> extends Table
56  {
57  
58      /** Base columns. */
59      private static Collection<Column<?>> BASE_COLUMNS = new LinkedHashSet<>();
60  
61      /** Extended data types, in order of relevant columns. */
62      private final List<ExtendedDataType<?, ?, ?, G>> extendedDataTypes;
63  
64      /** Filter data types, in order of relevant columns. */
65      private final List<FilterDataType<?>> filterDataTypes;
66  
67      /** Map with all sampling data. */
68      private final Map<LaneData<?>, TrajectoryGroup<G>> trajectories = new LinkedHashMap<>();
69  
70      static
71      {
72          BASE_COLUMNS.add(new Column<>("traj#", "Trajectory number", Integer.class, null));
73          BASE_COLUMNS.add(new Column<>("linkId", "Link id", String.class, null));
74          BASE_COLUMNS.add(new Column<>("laneId", "Lane id", String.class, null));
75          BASE_COLUMNS.add(new Column<>("gtuId", "GTU id", String.class, null));
76          BASE_COLUMNS.add(new Column<>("t", "Simulation time", FloatDuration.class, DurationUnit.SI.getId()));
77          BASE_COLUMNS.add(new Column<>("x", "Position on the lane", FloatLength.class, LengthUnit.SI.getId()));
78          BASE_COLUMNS.add(new Column<>("v", "Speed", FloatSpeed.class, SpeedUnit.SI.getId()));
79          BASE_COLUMNS.add(new Column<>("a", "Acceleration", FloatAcceleration.class, AccelerationUnit.SI.getId()));
80      }
81  
82      /**
83       * Constructor.
84       * @param extendedDataTypes Set&lt;? extends ExtendedDataType&lt;?, ?, ?, G&gt;&gt;; extended data types.
85       * @param filterDataTypes Set&lt;FilterDataType&lt;?&gt;&gt;; filter data types.
86       */
87      public SamplerData(final Set<ExtendedDataType<?, ?, ?, G>> extendedDataTypes, final Set<FilterDataType<?>> filterDataTypes)
88      {
89          super("sampler", "Trajectory data", generateColumns(extendedDataTypes, filterDataTypes));
90          /*
91           * The delivered types may not have a consistent iteration order. We need to store them in a data structure that does.
92           * The order in which we add them needs to be consistent with the columns generated, where we skip the 8 base columns.
93           */
94          this.extendedDataTypes = new ArrayList<>(extendedDataTypes.size());
95          for (int i = BASE_COLUMNS.size(); i < BASE_COLUMNS.size() + extendedDataTypes.size(); i++)
96          {
97              String columnId = getColumn(i).getId();
98              for (ExtendedDataType<?, ?, ?, G> extendedDataType : extendedDataTypes)
99              {
100                 if (extendedDataType.getId().equals(columnId))
101                 {
102                     this.extendedDataTypes.add(extendedDataType);
103                 }
104             }
105         }
106         this.filterDataTypes = new ArrayList<>(filterDataTypes.size());
107         for (int i = BASE_COLUMNS.size() + extendedDataTypes.size(); i < BASE_COLUMNS.size() + extendedDataTypes.size()
108                 + filterDataTypes.size(); i++)
109         {
110             String columnId = getColumn(i).getId();
111             for (FilterDataType<?> filterType : filterDataTypes)
112             {
113                 if (filterType.getId().equals(columnId))
114                 {
115                     this.filterDataTypes.add(filterType);
116                 }
117             }
118         }
119     }
120 
121     /**
122      * Generates the columns based on base information and the extended and filter types.
123      * @param extendedDataTypes Set&lt;? extends ExtendedDataType&lt;?, ?, ?, ? extends GtuData&gt;&gt;; extended data types.
124      * @param filterDataTypes Set&lt;FilterDataType&lt;?&gt;&gt;; filter data types.
125      * @return Collection&lt;Column&lt;?&gt;&gt;; columns.
126      */
127     private static Collection<Column<?>> generateColumns(
128             final Set<? extends ExtendedDataType<?, ?, ?, ? extends GtuData>> extendedDataTypes,
129             final Set<FilterDataType<?>> filterDataTypes)
130     {
131         Collection<Column<?>> out = new ArrayList<>(BASE_COLUMNS.size() + extendedDataTypes.size() + filterDataTypes.size());
132         out.addAll(BASE_COLUMNS);
133         for (ExtendedDataType<?, ?, ?, ?> extendedDataType : extendedDataTypes)
134         {
135             out.add(new Column<>(extendedDataType.getId(), extendedDataType.getDescription(), extendedDataType.getType(),
136                     getUnit(extendedDataType)));
137         }
138         for (FilterDataType<?> filterDataType : filterDataTypes)
139         {
140             out.add(new Column<>(filterDataType.getId(), filterDataType.getDescription(), String.class, null));
141         }
142         return out;
143     }
144 
145     /**
146      * Returns the unit for values in an extended data type.
147      * @param extendedDataType ExtendedDataType&lt;?, ?, ?, ?&gt;; extended data type.
148      * @return String; representation of the unit
149      */
150     private static String getUnit(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
151     {
152         if (Scalar.class.isAssignableFrom(extendedDataType.getType()))
153         {
154             try
155             {
156                 Class<?> unitClass = Class.forName(
157                         "org.djunits.unit." + extendedDataType.getType().getSimpleName().replace("Float", "") + "Unit");
158                 Field field = null;
159                 for (Field f : unitClass.getFields())
160                 {
161                     if (f.getName().equals("SI"))
162                     {
163                         field = f;
164                         break;
165                     }
166                     else if (f.getName().equals("DEFAULT"))
167                     {
168                         field = f;
169                     }
170                 }
171                 return field == null ? null : ((Unit<?>) field.get(unitClass)).getId();
172             }
173             catch (ClassNotFoundException | IllegalArgumentException | IllegalAccessException | SecurityException exception)
174             {
175                 return null;
176             }
177         }
178         return null;
179     }
180 
181     /**
182      * Stores a trajectory group with the lane direction.
183      * @param lane LaneData; lane direction
184      * @param trajectoryGroup trajectory group for given lane direction
185      */
186     protected final void putTrajectoryGroup(final LaneData<?> lane, final TrajectoryGroup<G> trajectoryGroup)
187     {
188         this.trajectories.put(lane, trajectoryGroup);
189     }
190 
191     /**
192      * Returns the set of lane directions.
193      * @return Set&lt;LaneData&gt;; lane directions
194      */
195     public final Set<LaneData<?>> getLanes()
196     {
197         return this.trajectories.keySet();
198     }
199 
200     /**
201      * Returns whether there is data for the give lane.
202      * @param lane LaneData; lane
203      * @return whether there is data for the give lane
204      */
205     public final boolean contains(final LaneData<?> lane)
206     {
207         return this.trajectories.containsKey(lane);
208     }
209 
210     /**
211      * Returns the trajectory group of given lane.
212      * @param lane LaneData; lane
213      * @return trajectory group of given lane, {@code null} if none
214      */
215     public final TrajectoryGroup<G> getTrajectoryGroup(final LaneData<?> lane)
216     {
217         return this.trajectories.get(lane);
218     }
219 
220     /**
221      * Write the contents of the sampler in to a file. By default this is zipped.
222      * @param file String; file
223      */
224     public final void writeToFile(final String file)
225     {
226         writeToFile(file, Compression.ZIP);
227     }
228 
229     /**
230      * Write the contents of the sampler in to a file.
231      * @param file String; file
232      * @param compression Compression; how to compress the data
233      */
234     public final void writeToFile(final String file, final Compression compression)
235     {
236         try
237         {
238             if (compression.equals(Compression.ZIP))
239             {
240                 String name = new File(file).getName();
241                 String csvName = name.toLowerCase().endsWith(".zip") ? name.substring(0, name.length() - 4) : name;
242                 CsvData.writeZippedData(new CompressedFileWriter(file), csvName, csvName + ".header", this);
243             }
244             else
245             {
246                 CsvData.writeData(file, file + ".header", this);
247             }
248         }
249         catch (IOException | TextSerializationException exception)
250         {
251             throw new RuntimeException("Unable to write sampler data.", exception);
252         }
253     }
254 
255     /** {@inheritDoc} */
256     @Override
257     public Iterator<Row> iterator()
258     {
259         return new SamplerDataIterator();
260     }
261 
262     /** {@inheritDoc} */
263     @Override
264     public boolean isEmpty()
265     {
266         for (TrajectoryGroup<G> group : this.trajectories.values())
267         {
268             for (Trajectory<G> trajectory : group.getTrajectories())
269             {
270                 if (trajectory.size() > 0)
271                 {
272                     return false;
273                 }
274             }
275         }
276         return true;
277     }
278 
279     /**
280      * Iterator over the sampler data. It iterates over lanes, trajectories on a lane, and indices within the trajectory.
281      * <p>
282      * Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
283      * <br>
284      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
285      * </p>
286      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
287      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
288      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
289      */
290     private final class SamplerDataIterator implements Iterator<Row>
291     {
292         /** Iterator over the sampled lanes. */
293         private Iterator<Entry<LaneData<?>, TrajectoryGroup<G>>> laneIterator =
294                 SamplerData.this.trajectories.entrySet().iterator();
295 
296         /** Current lane. */
297         private LaneData<?> currentLane;
298 
299         /** Iterator over trajectories on a lane. */
300         private Iterator<Trajectory<G>> trajectoryIterator = Collections.emptyIterator();
301 
302         /** Current trajectory. */
303         private Trajectory<G> currentTrajectory;
304 
305         /** Size of current trajectory, to check concurrent modification. */
306         private int currentTrajectorySize = 0;
307 
308         /** Trajectory counter (first column). */
309         private int trajectoryCounter = 0;
310 
311         /** Iterator over indices in a trajectory. */
312         private Iterator<Integer> indexIterator = Collections.emptyIterator();
313 
314         /** {@inheritDoc} */
315         @Override
316         public boolean hasNext()
317         {
318             while (!this.indexIterator.hasNext())
319             {
320                 while (!this.trajectoryIterator.hasNext())
321                 {
322                     if (!this.laneIterator.hasNext())
323                     {
324                         return false;
325                     }
326                     Entry<LaneData<?>, TrajectoryGroup<G>> entry = this.laneIterator.next();
327                     this.currentLane = entry.getKey();
328                     this.trajectoryIterator = entry.getValue().iterator();
329                 }
330                 this.currentTrajectory = this.trajectoryIterator.next();
331                 this.currentTrajectorySize = this.currentTrajectory.size();
332                 this.trajectoryCounter++;
333                 this.indexIterator = IntStream.range(0, this.currentTrajectory.size()).iterator();
334             }
335             return true;
336         }
337 
338         /** {@inheritDoc} */
339         @Override
340         public Row next()
341         {
342             Throw.when(!hasNext(), NoSuchElementException.class, "Sampler data has no next row.");
343             Throw.when(this.currentTrajectory.size() != this.currentTrajectorySize, ConcurrentModificationException.class,
344                     "Trajectory modified while iterating.");
345 
346             int trajectoryIndex = this.indexIterator.next();
347             try
348             {
349                 // base data
350                 Object[] data = getBaseData(trajectoryIndex);
351                 int dataIndex = BASE_COLUMNS.size();
352 
353                 // extended data
354                 for (int i = 0; i < SamplerData.this.extendedDataTypes.size(); i++)
355                 {
356                     ExtendedDataType<?, ?, ?, G> extendedDataType = SamplerData.this.extendedDataTypes.get(i);
357                     data[dataIndex++] = this.currentTrajectory.contains(extendedDataType)
358                             ? this.currentTrajectory.getExtendedData(extendedDataType, trajectoryIndex) : null;
359                 }
360 
361                 // filter data
362                 for (int i = 0; i < SamplerData.this.filterDataTypes.size(); i++)
363                 {
364                     FilterDataType<?> filterDataType = SamplerData.this.filterDataTypes.get(i);
365                     // filter data is only stored on the first index, as this data is fixed over a trajectory
366                     data[dataIndex++] = trajectoryIndex == 0 && this.currentTrajectory.contains(filterDataType)
367                             ? this.currentTrajectory.getFilterData(filterDataType) : null;
368                 }
369 
370                 return new Row(SamplerData.this, data);
371             }
372             catch (SamplingException se)
373             {
374                 throw new RuntimeException("Sampling exception during iteration over sampler data.", se);
375             }
376         }
377 
378         /**
379          * Returns an array with the base data. The array is of size to also contain the extended and filter data.
380          * @param trajectoryIndex int; trajectory index in the current trajectory.
381          * @return Object[] base data of size to also contain the extended and filter data.
382          * @throws SamplingException if data can not be obtained.
383          */
384         private Object[] getBaseData(final int trajectoryIndex) throws SamplingException
385         {
386             Object[] data = new Object[SamplerData.this.getNumberOfColumns()];
387             int dataIndex = 0;
388             for (Column<?> column : BASE_COLUMNS)
389             {
390                 switch (column.getId())
391                 {
392                     case "traj#":
393                         data[dataIndex] = this.trajectoryCounter;
394                         break;
395                     case "linkId":
396                         data[dataIndex] = this.currentLane.getLinkData().getId();
397                         break;
398                     case "laneId":
399                         data[dataIndex] = this.currentLane.getId();
400                         break;
401                     case "gtuId":
402                         data[dataIndex] = this.currentTrajectory.getGtuId();
403                         break;
404                     case "t":
405                         data[dataIndex] = FloatDuration.instantiateSI(this.currentTrajectory.getT(trajectoryIndex));
406                         break;
407                     case "x":
408                         data[dataIndex] = FloatLength.instantiateSI(this.currentTrajectory.getX(trajectoryIndex));
409                         break;
410                     case "v":
411                         data[dataIndex] = FloatSpeed.instantiateSI(this.currentTrajectory.getV(trajectoryIndex));
412                         break;
413                     case "a":
414                         data[dataIndex] = FloatAcceleration.instantiateSI(this.currentTrajectory.getA(trajectoryIndex));
415                         break;
416                     default:
417 
418                 }
419                 dataIndex++;
420             }
421             return data;
422         }
423     }
424 
425     /**
426      * Compression method.
427      * <p>
428      * Copyright (c) 2022-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
429      * <br>
430      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
431      * </p>
432      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
433      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
434      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
435      */
436     public enum Compression
437     {
438         /** No compression. */
439         NONE,
440 
441         /** Zip compression. */
442         ZIP
443     }
444 
445 }