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