package org.opentrafficsim.kpi.sampling; import java.io.BufferedWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import org.djunits.value.vdouble.scalar.Length; import org.opentrafficsim.base.compressedfiles.CompressedFileWriter; import org.opentrafficsim.kpi.interfaces.GtuDataInterface; import org.opentrafficsim.kpi.interfaces.GtuTypeDataInterface; import org.opentrafficsim.kpi.interfaces.LaneDataInterface; import org.opentrafficsim.kpi.interfaces.LinkDataInterface; import org.opentrafficsim.kpi.interfaces.NodeDataInterface; import org.opentrafficsim.kpi.interfaces.RouteDataInterface; import org.opentrafficsim.kpi.sampling.data.ExtendedDataType; import org.opentrafficsim.kpi.sampling.meta.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. *

* Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel * @param gtu data type */ // TODO: extending list table requires us to know the columns beforehand, create a view asTable()? public class SamplerData extends AbstractTable { /** * Constructor. * @param columns Collection<Column<?>>; columns */ public SamplerData(final Collection> columns) { super("sampler", "Trajectory data", columns); } /** Map with all sampling data. */ private final Map> trajectories = new LinkedHashMap<>(); /** * Stores a trajectory group with the lane direction. * @param kpiLaneDirection KpiLaneDirection; lane direction * @param trajectoryGroup TrajectoryGroup<G>; trajectory group for given lane direction */ protected final void putTrajectoryGroup(final KpiLane kpiLaneDirection, final TrajectoryGroup trajectoryGroup) { this.trajectories.put(kpiLaneDirection, trajectoryGroup); } /** * Returns the set of lane directions. * @return Set<KpiLaneDirection>; lane directions */ public final Set getLaneDirections() { return this.trajectories.keySet(); } /** * Returns whether there is data for the give lane direction. * @param kpiLaneDirection KpiLaneDirection; lane direction * @return whether there is data for the give lane direction */ public final boolean contains(final KpiLane kpiLaneDirection) { return this.trajectories.containsKey(kpiLaneDirection); } /** * Returns the trajectory group of given lane direction. * @param kpiLaneDirection KpiLaneDirection; lane direction * @return trajectory group of given lane direction, {@code null} if none */ public final TrajectoryGroup getTrajectoryGroup(final KpiLane kpiLaneDirection) { return this.trajectories.get(kpiLaneDirection); } /** * Write the contents of the sampler in to a file. By default this is zipped and numeric data is formated %.3f. * @param file String; file */ public final void writeToFile(final String file) { writeToFile(file, "%.3f", CompressionMethod.ZIP); } /** * Write the contents of the sampler in to a file. * @param file String; file * @param format String; number format, as used in {@code String.format()} * @param compression CompressionMethod; how to compress the data */ public final void writeToFile(final String file, final String format, final CompressionMethod compression) { int counter = 0; BufferedWriter bw = CompressedFileWriter.create(file, compression.equals(CompressionMethod.ZIP)); // TODO: Sampler used this to cut-off space if SpaceTimeRegion's did not cover complete lanes. Trajectories are however // recorded over the complete length. /* * // create Query, as this class is designed to filter for space-time regions Query query = new Query<>(this, "", * new MetaDataSet()); for (SpaceTimeRegion str : this.spaceTimeRegions) { * query.addSpaceTimeRegion(str.getLaneDirection(), str.getStartPosition(), str.getEndPosition(), str.getStartTime(), * str.getEndTime()); } List> groups = * query.getTrajectoryGroups(Time.instantiateSI(Double.POSITIVE_INFINITY)); */ Collection> groups = this.trajectories.values(); try { // gather all filter data types for the header line List> allFilterDataTypes = new ArrayList<>(); for (TrajectoryGroup group : groups) { for (Trajectory trajectory : group.getTrajectories()) { for (FilterDataType filterDataType : trajectory.getFilterDataTypes()) { if (!allFilterDataTypes.contains(filterDataType)) { allFilterDataTypes.add(filterDataType); } } } } // gather all extended data types for the header line List> allExtendedDataTypes = new ArrayList<>(); for (TrajectoryGroup group : groups) { for (Trajectory trajectory : group.getTrajectories()) { for (ExtendedDataType extendedDataType : trajectory.getExtendedDataTypes()) { if (!allExtendedDataTypes.contains(extendedDataType)) { allExtendedDataTypes.add(extendedDataType); } } } } // create header line StringBuilder str = new StringBuilder(); str.append("traj#,linkId,laneId&dir,gtuId,t,x,v,a"); for (FilterDataType metaDataType : allFilterDataTypes) { str.append(","); str.append(metaDataType.getId()); } for (ExtendedDataType extendedDataType : allExtendedDataTypes) { str.append(","); str.append(extendedDataType.getId()); } bw.write(str.toString()); bw.newLine(); for (TrajectoryGroup group : groups) { for (Trajectory trajectory : group.getTrajectories()) { counter++; float[] t = trajectory.getT(); float[] x = trajectory.getX(); float[] v = trajectory.getV(); float[] a = trajectory.getA(); Map, Object> extendedData = new LinkedHashMap<>(); for (ExtendedDataType extendedDataType : allExtendedDataTypes) { if (trajectory.contains(extendedDataType)) { try { extendedData.put(extendedDataType, trajectory.getExtendedData(extendedDataType)); } catch (SamplingException exception) { // should not occur, we obtain the extended data types from the trajectory throw new RuntimeException("Error while loading extended data type.", exception); } } } for (int i = 0; i < t.length; i++) { // TODO: values can contain ","; use csv writer str = new StringBuilder(); str.append(counter); str.append(","); if (!compression.equals(CompressionMethod.OMIT_DUPLICATE_INFO) || i == 0) { str.append(group.getLaneDirection().getLaneData().getLinkData().getId()); str.append(","); str.append(group.getLaneDirection().getLaneData().getId()); str.append(","); str.append(trajectory.getGtuId()); str.append(","); } else { // one trajectory is on the same lane and pertains to the same GTU, no need to repeat data str.append(",,,"); } str.append(String.format(format, t[i])); str.append(","); str.append(String.format(format, x[i])); str.append(","); str.append(String.format(format, v[i])); str.append(","); str.append(String.format(format, a[i])); for (FilterDataType metaDataType : allFilterDataTypes) { str.append(","); if (i == 0 && trajectory.contains(metaDataType)) { // no need to repeat meta data str.append(metaDataType.formatValue(format, castValue(trajectory.getMetaData(metaDataType)))); } } for (ExtendedDataType extendedDataType : allExtendedDataTypes) { str.append(","); if (trajectory.contains(extendedDataType)) { try { str.append( extendedDataType.formatValue(format, castValue(extendedData, extendedDataType, i))); } catch (SamplingException exception) { // should not occur, we obtain the extended data types from the trajectory throw new RuntimeException("Error while loading extended data type.", exception); } } } bw.write(str.toString()); bw.newLine(); } } } } catch (IOException exception) { throw new RuntimeException("Could not write to file.", exception); } // close file on fail finally { try { if (bw != null) { bw.close(); } } catch (IOException ex) { ex.printStackTrace(); } } } /** * Cast value to type for meta data. * @param value Object; value object to cast * @return cast value * @param type of value */ @SuppressWarnings("unchecked") private T castValue(final Object value) { return (T) value; } /** * Cast value to type for extended data. * @param extendedData Map<ExtendedDataType<?,?,?,?>,Object>; extended data of trajectory in output form * @param extendedDataType ExtendedDataType<?,?,?,?>; extended data type * @param i int; index of value to return * @return cast value * @throws SamplingException when the found index is out of bounds * @param type of value * @param output type * @param storage type */ @SuppressWarnings("unchecked") private T castValue(final Map, Object> extendedData, final ExtendedDataType extendedDataType, final int i) throws SamplingException { // is only called on value directly taken from an ExtendedDataType within range of trajectory ExtendedDataType edt = (ExtendedDataType) extendedDataType; return edt.getOutputValue((O) extendedData.get(edt), i); } /** * Defines the compression method for stored data. *

* Copyright (c) 2013-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 3 mei 2017
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel */ public enum CompressionMethod { /** No compression. */ NONE, /** Duplicate info per trajectory is only stored at the first sample, and empty for other samples. */ OMIT_DUPLICATE_INFO, /** Zip compression. */ ZIP, } /** * Loads sampler data from a file. There are a few limitations with respect to live sampled data: *

    *
  1. The number of decimals in numeric data is equal to the stored format.
  2. *
  3. All extended data types are stored as {@code String}.
  4. *
  5. Meta data types are not recognized, and hence stored as extended data types. Values are always stored as * {@code String}.
  6. *
* @param file String; file * @return Sampler data from file */ public static SamplerData loadFromFile(final String file) { return loadFromFile(file, new LinkedHashSet>(), new LinkedHashSet>()); } /** * Loads sampler data from a file. There are a few limitations with respect to live sampled data: *
    *
  1. The number of decimals in numeric data is equal to the stored format.
  2. *
  3. All extended data types are stored as {@code String}, unless recognized by id as provided.
  4. *
  5. Meta data types are not recognized, and hence stored as extended data types, unless recognized by id as provided. * Values are always stored as {@code String}.
  6. *
* @param file String; file * @param extendedDataTypes Set<ExtendedDataType<?, ?, ?, ?>>; extended data types * @param metaDataTypes Set<FilterDataType<?>>; meta data types * @return Sampler data from file */ @SuppressWarnings("unchecked") public static SamplerData loadFromFile(final String file, final Set> extendedDataTypes, final Set> metaDataTypes) { /* * @SuppressWarnings("rawtypes") SamplerData samplerData = new SamplerData(); // "traj#,linkId,laneId&dir,gtuId,t,x,v,a" * meta data types, extended data types // we can use the default meta data types: cross section, destination, origin, * route and GTU type Getter nodes = new Getter((id) -> new NodeData(id)); Getter * gtuTypes = new Getter((id) -> new GtuTypeData(id)); Getter routes = new * Getter((id) -> new RouteData(id)); Getter links = new Getter((id) -> new * LinkData(id)); BiGetter lanes = new BiGetter((id, link) -> new LaneData(id, * link)); BiGetter laneDirections = new BiGetter((dir, lane) -> * new KpiLaneDirection(lane, dir.equals("+") ? KpiGtuDirectionality.DIR_PLUS : KpiGtuDirectionality.DIR_MINUS)); * @SuppressWarnings("rawtypes") Function groupFunction = (laneDir) -> new * TrajectoryGroup(Time.ZERO, laneDir); String id = null; if (!gtus.containsKey(id)) { // NOTE: USE SEPARATE IDS HERE * gtus.put(id, new GtuData(id, nodeSupplier.apply(id), nodeSupplier.apply(id), gtuTypeSupplier.apply(id), * routeSupplier.apply(id))); } GtuData gtuData = gtus.get(id); Trajectory trajectory = new Trajectory(gtuData, * metaData, extendedDataTypes, kpiLaneDirection); // TODO: set data from outside trajectory.add(position, speed, * acceleration, time, gtu); KpiLaneDirection laneDir = null; ((TrajectoryGroup) * samplerData.trajectories.computeIfAbsent(laneDir, groupFunction)).addTrajectory(trajectory); return samplerData; */ return null; } /** * Returns a value from the map. Creates a value if needed. * @param id String; id of object (key in map) * @param map Map<String, T>; stored values * @param producer Function<String, T>; producer used if no value exists in the map * @param type * @return value for the id */ private final T getOrCreate(final String id, final Map map, final Function producer) { if (!map.containsKey(id)) { map.put(id, producer.apply(id)); } return map.get(id); } // TABLE METHODS /** {@inheritDoc} */ @Override public Iterator iterator() { // TODO: local iterator over this.trajectories, trajectories per group, and length of each trajectory // TODO: gathering the extended and filter data types should be done here, these are within the trajectories, and upon // file loading, this should be mimicked Iterator laneIterator = this.trajectories.keySet().iterator(); return new Iterator() { private Iterator> trajectoryIterator = laneIterator.hasNext() ? SamplerData.this.trajectories.get(laneIterator.next()).iterator() : null; private Trajectory trajectory = this.trajectoryIterator != null && this.trajectoryIterator.hasNext() ? this.trajectoryIterator.next() : null; private Trajectory currentTrajectory; private int index; @Override public boolean hasNext() { if (this.index == this.currentTrajectory.size()) { // get next trajectory } return true; } @Override public Record next() { Record record = new Record() { @Override public T getValue(final Column column) { return null; } @Override public Object getValue(final String id) { return null; } }; this.index++; return record; } }; } /** {@inheritDoc} */ @Override public boolean isEmpty() { for (TrajectoryGroup group : this.trajectories.values()) { for (Trajectory trajectory : group.getTrajectories()) { if (trajectory.size() > 0) { return false; } } } return true; } // LOCAL HELPER CLASSES TO IMPLEMENT INTERFACES // /** * Getter for single {@code String} input. * @param output value type */ private static class Getter { /** Map with cached values. */ private final Map map = new LinkedHashMap<>(); /** Provider function. */ private Function function; /** * Constructor. * @param function Function<String, T>; provider function */ Getter(final Function function) { this.function = function; } /** * Get value, from cache or provider function. * @param id String; id * @return T; value, from cache or provider function */ public T get(final String id) { T t; if (!this.map.containsKey(id)) { t = this.function.apply(id); this.map.put(id, t); } else { t = this.map.get(id); } return t; } } /** * Getter for dual {@code String} and {@code O} input. * @param type of second input (besides the first being {@code String}) * @param output value type */ private static class BiGetter { /** Map with cached values. */ private final Map map = new LinkedHashMap<>(); /** Provider function. */ private BiFunction function; /** * Constructor. * @param function BiFunction<String, O, T>; provider function */ BiGetter(final BiFunction function) { this.function = function; } /** * Get value, from cache or provider function. * @param id String; id * @param o O; other object * @return T; value, from cache or provider function */ public T get(final String id, final O o) { T t; if (!this.map.containsKey(id)) { t = this.function.apply(id, o); this.map.put(id, t); } else { t = this.map.get(id); } return t; } } /** Helper class LinkData. */ private static class LinkData implements LinkDataInterface { /** Length ({@code null} always). */ private final Length length = null; // unknown in this context /** Id. */ private final String id; /** Lanes. */ private final List lanes = new ArrayList<>(); /** * @param id String; id */ LinkData(final String id) { this.id = id; } /** {@inheritDoc} */ @Override public Length getLength() { return this.length; } /** {@inheritDoc} */ @Override public List getLaneDatas() { return this.lanes; } /** {@inheritDoc} */ @Override public String getId() { return this.id; } } /** Helper class LaneData. */ private static class LaneData implements LaneDataInterface { /** Length ({@code null} always). */ private final Length length = null; // unknown in this context /** Id. */ private final String id; /** Link. */ private final LinkData link; /** * Constructor. * @param id String; id * @param link LinkData; link */ @SuppressWarnings("synthetic-access") LaneData(final String id, final LinkData link) { this.id = id; this.link = link; link.lanes.add(this); } /** {@inheritDoc} */ @Override public Length getLength() { return this.length; } /** {@inheritDoc} */ @Override public LinkData getLinkData() { return this.link; } /** {@inheritDoc} */ @Override public String getId() { return this.id; } } /** Helper class NodeData. */ private static class NodeData implements NodeDataInterface { /** Node id. */ private String id; /** * Constructor. * @param id String; id */ NodeData(final String id) { this.id = id; } /** {@inheritDoc} */ @Override public String getId() { return null; } } /** Helper class GtuTypeData. */ private static class GtuTypeData implements GtuTypeDataInterface { /** Node id. */ private String id; /** * Constructor. * @param id String; id */ GtuTypeData(final String id) { this.id = id; } /** {@inheritDoc} */ @Override public String getId() { return null; } } /** Helper class RouteData. */ private static class RouteData implements RouteDataInterface { /** Node id. */ private String id; /** * Constructor. * @param id String; id */ RouteData(final String id) { this.id = id; } /** {@inheritDoc} */ @Override public String getId() { return null; } } /** Helper class GtuData. */ private static class GtuData implements GtuDataInterface { /** Id. */ private final String id; /** Origin. */ private final NodeData origin; /** Destination. */ private final NodeData destination; /** GTU type. */ private final GtuTypeData gtuType; /** Route. */ private final RouteData route; /** * @param id String; id * @param origin NodeData; origin * @param destination NodeData; destination * @param gtuType GtuTypeData; GTU type * @param route RouteData; route */ GtuData(final String id, final NodeData origin, final NodeData destination, final GtuTypeData gtuType, final RouteData route) { this.id = id; this.origin = origin; this.destination = destination; this.gtuType = gtuType; this.route = route; } /** {@inheritDoc} */ @Override public String getId() { return this.id; } /** {@inheritDoc} */ @Override public NodeDataInterface getOriginNodeData() { return this.origin; } /** {@inheritDoc} */ @Override public NodeDataInterface getDestinationNodeData() { return this.destination; } /** {@inheritDoc} */ @Override public GtuTypeDataInterface getGtuTypeData() { return this.gtuType; } /** {@inheritDoc} */ @Override public RouteDataInterface getRouteData() { return this.route; } } }