SamplerData.java
package org.opentrafficsim.kpi.sampling;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.IntStream;
import org.djunits.unit.AccelerationUnit;
import org.djunits.unit.DurationUnit;
import org.djunits.unit.LengthUnit;
import org.djunits.unit.SpeedUnit;
import org.djunits.unit.Unit;
import org.djunits.value.base.Scalar;
import org.djunits.value.vfloat.scalar.FloatAcceleration;
import org.djunits.value.vfloat.scalar.FloatDuration;
import org.djunits.value.vfloat.scalar.FloatLength;
import org.djunits.value.vfloat.scalar.FloatSpeed;
import org.djutils.data.Column;
import org.djutils.data.Row;
import org.djutils.data.Table;
import org.djutils.data.csv.CsvData;
import org.djutils.data.serialization.TextSerializationException;
import org.djutils.exceptions.Throw;
import org.djutils.io.CompressedFileWriter;
import org.opentrafficsim.kpi.interfaces.GtuData;
import org.opentrafficsim.kpi.interfaces.LaneData;
import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
import org.opentrafficsim.kpi.sampling.filter.FilterDataType;
/**
* SamplerData is a storage for trajectory data. Adding trajectory groups can only be done by subclasses. This is however not a
* guaranteed read-only class. Any type can obtain the lane directions and with those the coupled trajectory groups.
* Trajectories can be added to these trajectory groups. Data can also be added to the trajectories themselves.
* <p>
* Copyright (c) 2020-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
* @param <G> GTU data type
*/
public class SamplerData<G extends GtuData> extends Table
{
/** Base columns. */
private static final Collection<Column<?>> BASE_COLUMNS = new LinkedHashSet<>();
/** Extended data types, in order of relevant columns. */
private final List<ExtendedDataType<?, ?, ?, ? super G>> extendedDataTypes;
/** Filter data types, in order of relevant columns. */
private final List<FilterDataType<?, ? super G>> filterDataTypes;
/** Map with all sampling data. */
private final Map<LaneData<?>, TrajectoryGroup<G>> trajectories = new LinkedHashMap<>();
static
{
BASE_COLUMNS.add(new Column<>("traj#", "Trajectory number", Integer.class, null));
BASE_COLUMNS.add(new Column<>("linkId", "Link id", String.class, null));
BASE_COLUMNS.add(new Column<>("laneId", "Lane id", String.class, null));
BASE_COLUMNS.add(new Column<>("gtuId", "GTU id", String.class, null));
BASE_COLUMNS.add(new Column<>("t", "Simulation time", FloatDuration.class, DurationUnit.SI.getId()));
BASE_COLUMNS.add(new Column<>("x", "Position on the lane", FloatLength.class, LengthUnit.SI.getId()));
BASE_COLUMNS.add(new Column<>("v", "Speed", FloatSpeed.class, SpeedUnit.SI.getId()));
BASE_COLUMNS.add(new Column<>("a", "Acceleration", FloatAcceleration.class, AccelerationUnit.SI.getId()));
}
/**
* Constructor.
* @param extendedDataTypes extended data types
* @param filterDataTypes filter data types
*/
public SamplerData(final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedDataTypes,
final Set<FilterDataType<?, ? super G>> filterDataTypes)
{
super("sampler", "Trajectory data", generateColumns(extendedDataTypes, filterDataTypes));
/*
* The delivered types may not have a consistent iteration order. We need to store them in a data structure that does.
* The order in which we add them needs to be consistent with the columns generated, where we skip the 8 base columns.
*/
this.extendedDataTypes = new ArrayList<>(extendedDataTypes.size());
for (int i = BASE_COLUMNS.size(); i < BASE_COLUMNS.size() + extendedDataTypes.size(); i++)
{
String columnId = getColumn(i).getId();
for (ExtendedDataType<?, ?, ?, ? super G> extendedDataType : extendedDataTypes)
{
if (extendedDataType.getId().equals(columnId))
{
this.extendedDataTypes.add(extendedDataType);
}
}
}
this.filterDataTypes = new ArrayList<>(filterDataTypes.size());
for (int i = BASE_COLUMNS.size() + extendedDataTypes.size(); i < BASE_COLUMNS.size() + extendedDataTypes.size()
+ filterDataTypes.size(); i++)
{
String columnId = getColumn(i).getId();
for (FilterDataType<?, ? super G> filterType : filterDataTypes)
{
if (filterType.getId().equals(columnId))
{
this.filterDataTypes.add(filterType);
}
}
}
}
/**
* Generates the columns based on base information and the extended and filter types.
* @param extendedDataTypes2 extended data types
* @param filterDataTypes2 filter data types
* @param <G2> type to bound extended and filter data types, different from G as this is a static method
* @return columns.
*/
private static <G2> Collection<Column<?>> generateColumns(
final Set<ExtendedDataType<?, ?, ?, ? super G2>> extendedDataTypes2,
final Set<FilterDataType<?, ? super G2>> filterDataTypes2)
{
Collection<Column<?>> out = new ArrayList<>(BASE_COLUMNS.size() + extendedDataTypes2.size() + filterDataTypes2.size());
out.addAll(BASE_COLUMNS);
for (ExtendedDataType<?, ?, ?, ?> extendedDataType : extendedDataTypes2)
{
out.add(new Column<>(extendedDataType.getId(), extendedDataType.getDescription(), extendedDataType.getType(),
getUnit(extendedDataType)));
}
for (FilterDataType<?, ?> filterDataType : filterDataTypes2)
{
out.add(new Column<>(filterDataType.getId(), filterDataType.getDescription(), filterDataType.getType(),
getUnit(filterDataType)));
}
return out;
}
/**
* Returns the unit for values in an extended data type.
* @param extendedDataType extended data type.
* @return representation of the unit
*/
private static String getUnit(final DataType<?, ?> extendedDataType)
{
if (Scalar.class.isAssignableFrom(extendedDataType.getType()))
{
try
{
Class<?> unitClass = Class.forName(
"org.djunits.unit." + extendedDataType.getType().getSimpleName().replace("Float", "") + "Unit");
Field field = null;
for (Field f : unitClass.getFields())
{
if (f.getName().equals("SI"))
{
field = f;
break;
}
else if (f.getName().equals("DEFAULT"))
{
field = f;
}
}
return field == null ? null : ((Unit<?>) field.get(unitClass)).getId();
}
catch (ClassNotFoundException | IllegalArgumentException | IllegalAccessException | SecurityException exception)
{
return null;
}
}
return null;
}
/**
* Stores a trajectory group with the lane direction.
* @param lane lane direction
* @param trajectoryGroup trajectory group for given lane direction
*/
protected final void putTrajectoryGroup(final LaneData<?> lane, final TrajectoryGroup<G> trajectoryGroup)
{
this.trajectories.put(lane, trajectoryGroup);
}
/**
* Returns the set of lanes.
* @return lanes (safe copy)
*/
public final Set<LaneData<?>> getLanes()
{
return new LinkedHashSet<>(this.trajectories.keySet());
}
/**
* Returns whether there is data for the give lane.
* @param lane lane
* @return whether there is data for the give lane
*/
public final boolean contains(final LaneData<?> lane)
{
return this.trajectories.containsKey(lane);
}
/**
* Returns the trajectory group of given lane.
* @param lane lane
* @return trajectory group of given lane, {@code null} if none
*/
public final TrajectoryGroup<G> getTrajectoryGroup(final LaneData<?> lane)
{
return this.trajectories.get(lane);
}
/**
* Write the contents of the sampler in to a file. By default this is zipped.
* @param file file
*/
public final void writeToFile(final String file)
{
writeToFile(file, Compression.ZIP);
}
/**
* Write the contents of the sampler in to a file.
* @param file file
* @param compression how to compress the data
*/
public final void writeToFile(final String file, final Compression compression)
{
try
{
if (compression.equals(Compression.ZIP))
{
String name = new File(file).getName();
String csvName = name.toLowerCase().endsWith(".zip") ? name.substring(0, name.length() - 4) : name;
CsvData.writeZippedData(new CompressedFileWriter(file), csvName, csvName + ".header", this);
}
else
{
CsvData.writeData(file, file + ".header", this);
}
}
catch (IOException | TextSerializationException exception)
{
throw new RuntimeException("Unable to write sampler data.", exception);
}
}
@Override
public Iterator<Row> iterator()
{
return new SamplerDataIterator();
}
@Override
public boolean isEmpty()
{
for (TrajectoryGroup<G> group : this.trajectories.values())
{
for (Trajectory<G> trajectory : group.getTrajectories())
{
if (trajectory.size() > 0)
{
return false;
}
}
}
return true;
}
/**
* Iterator over the sampler data. It iterates over lanes, trajectories on a lane, and indices within the trajectory.
*/
private final class SamplerDataIterator implements Iterator<Row>
{
/** Iterator over the sampled lanes. */
private Iterator<Entry<LaneData<?>, TrajectoryGroup<G>>> laneIterator =
SamplerData.this.trajectories.entrySet().iterator();
/** Current lane. */
private LaneData<?> currentLane;
/** Iterator over trajectories on a lane. */
private Iterator<Trajectory<G>> trajectoryIterator = Collections.emptyIterator();
/** Current trajectory. */
private Trajectory<G> currentTrajectory;
/** Size of current trajectory, to check concurrent modification. */
private int currentTrajectorySize = 0;
/** Trajectory counter (first column). */
private int trajectoryCounter = 0;
/** Iterator over indices in a trajectory. */
private Iterator<Integer> indexIterator = Collections.emptyIterator();
@Override
public boolean hasNext()
{
while (!this.indexIterator.hasNext())
{
while (!this.trajectoryIterator.hasNext())
{
if (!this.laneIterator.hasNext())
{
return false;
}
Entry<LaneData<?>, TrajectoryGroup<G>> entry = this.laneIterator.next();
this.currentLane = entry.getKey();
this.trajectoryIterator = entry.getValue().iterator();
}
this.currentTrajectory = this.trajectoryIterator.next();
this.currentTrajectorySize = this.currentTrajectory.size();
this.trajectoryCounter++;
this.indexIterator = IntStream.range(0, this.currentTrajectory.size()).iterator();
}
return true;
}
@Override
public Row next()
{
Throw.when(!hasNext(), NoSuchElementException.class, "Sampler data has no next row.");
Throw.when(this.currentTrajectory.size() != this.currentTrajectorySize, ConcurrentModificationException.class,
"Trajectory modified while iterating.");
int trajectoryIndex = this.indexIterator.next();
// base data
Object[] data = getBaseData(trajectoryIndex);
int dataIndex = BASE_COLUMNS.size();
// extended data
for (int i = 0; i < SamplerData.this.extendedDataTypes.size(); i++)
{
ExtendedDataType<?, ?, ?, ?> extendedDataType = SamplerData.this.extendedDataTypes.get(i);
data[dataIndex++] = this.currentTrajectory.contains(extendedDataType)
? this.currentTrajectory.getExtendedData(extendedDataType, trajectoryIndex) : null;
}
// filter data
for (int i = 0; i < SamplerData.this.filterDataTypes.size(); i++)
{
FilterDataType<?, ?> filterDataType = SamplerData.this.filterDataTypes.get(i);
// filter data is only stored on the first index, as this data is fixed over a trajectory
data[dataIndex++] = trajectoryIndex == 0 && this.currentTrajectory.contains(filterDataType)
? this.currentTrajectory.getFilterData(filterDataType) : null;
}
return new Row(SamplerData.this, data);
}
/**
* Returns an array with the base data. The array is of size to also contain the extended and filter data.
* @param trajectoryIndex trajectory index in the current trajectory.
* @return Object[] base data of size to also contain the extended and filter data.
*/
private Object[] getBaseData(final int trajectoryIndex)
{
Object[] data = new Object[SamplerData.this.getNumberOfColumns()];
int dataIndex = 0;
for (Column<?> column : BASE_COLUMNS)
{
switch (column.getId())
{
case "traj#":
data[dataIndex] = this.trajectoryCounter;
break;
case "linkId":
data[dataIndex] = this.currentLane.getLinkData().getId();
break;
case "laneId":
data[dataIndex] = this.currentLane.getId();
break;
case "gtuId":
data[dataIndex] = this.currentTrajectory.getGtuId();
break;
case "t":
data[dataIndex] = FloatDuration.instantiateSI(this.currentTrajectory.getT(trajectoryIndex));
break;
case "x":
data[dataIndex] = FloatLength.instantiateSI(this.currentTrajectory.getX(trajectoryIndex));
break;
case "v":
data[dataIndex] = FloatSpeed.instantiateSI(this.currentTrajectory.getV(trajectoryIndex));
break;
case "a":
data[dataIndex] = FloatAcceleration.instantiateSI(this.currentTrajectory.getA(trajectoryIndex));
break;
default:
}
dataIndex++;
}
return data;
}
}
/**
* Compression method.
*/
public enum Compression
{
/** No compression. */
NONE,
/** Zip compression. */
ZIP
}
}