Injections.java

package org.opentrafficsim.road.gtu.generator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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.SortedSet;
import java.util.function.Function;
import java.util.function.Supplier;

import org.djunits.unit.DurationUnit;
import org.djunits.unit.LengthUnit;
import org.djunits.value.vdouble.scalar.Acceleration;
import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djunits.value.vdouble.scalar.Speed;
import org.djutils.data.ListTable;
import org.djutils.data.Row;
import org.djutils.data.Table;
import org.djutils.exceptions.Throw;
import org.djutils.immutablecollections.ImmutableLinkedHashMap;
import org.djutils.immutablecollections.ImmutableMap;
import org.djutils.multikeymap.MultiKeyMap;
import org.opentrafficsim.base.parameters.ParameterException;
import org.opentrafficsim.core.distributions.Generator;
import org.opentrafficsim.core.distributions.ProbabilityException;
import org.opentrafficsim.core.gtu.GtuCharacteristics;
import org.opentrafficsim.core.gtu.GtuException;
import org.opentrafficsim.core.gtu.GtuType;
import org.opentrafficsim.core.network.Link;
import org.opentrafficsim.core.network.Network;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.core.network.Node;
import org.opentrafficsim.core.network.route.Route;
import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator.Placement;
import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator.RoomChecker;
import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristics;
import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGenerator;
import org.opentrafficsim.road.gtu.lane.VehicleModel;
import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayGtu;
import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlannerFactory;
import org.opentrafficsim.road.network.lane.CrossSectionLink;
import org.opentrafficsim.road.network.lane.Lane;
import org.opentrafficsim.road.network.lane.LanePosition;
import org.pmw.tinylog.Logger;

import nl.tudelft.simulation.jstats.streams.StreamInterface;

/**
 * Injections can be used to have a large degree of control over GTU generation. Depending on the information provided in an
 * injections table, this class may be used in conjunction with {@code LaneBasedGtuGenerator} as a:
 * <ol>
 * <li>{@code Generator<Duration>} for inter-arrival times</li>
 * <li>{@code LaneBasedGtuCharacteristicsGenerator} through {@code asLaneBasedGtuCharacteristicsGenerator}</li>
 * <li>{@code GeneratorPositions}</li>
 * <li>{@code RoomChecker}</li>
 * <li>{@code Supplier<String>} for GTU ids</li>
 * </ol>
 * It is assumed that for each next GTU, first an inter-arrival time is requested. Functions 2 and 3 will not check order and
 * simply return information from the current row in the injections table. Function 4 and 5 are tracked independently and
 * asynchronous with the rest, as these occur at later times when GTUs are (attempted to be) placed.
 * <p>
 * Copyright (c) 2022-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/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public class Injections implements Generator<Duration>, Supplier<String>, GeneratorPositions, RoomChecker
{

    /** Time column id. */
    public static final String TIME_COLUMN = "time";

    /** Id column id. */
    public static final String ID_COLUMN = "id";

    /** GTU type column id. */
    public static final String GTU_TYPE_COLUMN = "gtuType";

    /** Position (on lane) column id. */
    public static final String POSITION_COLUMN = "position";

    /** Lane column id. */
    public static final String LANE_COLUMN = "lane";

    /** Link column id. */
    public static final String LINK_COLUMN = "link";

    /** Speed column id. */
    public static final String SPEED_COLUMN = "speed";

    /** Origin column id. */
    public static final String ORIGIN_COLUMN = "origin";

    /** Destination column id. */
    public static final String DESTINATION_COLUMN = "destination";

    /** Route column id. */
    public static final String ROUTE_COLUMN = "route";

    /** Length column id. */
    public static final String LENGTH_COLUMN = "length";

    /** Width column id. */
    public static final String WIDTH_COLUMN = "width";

    /** Maximum speed column id. */
    public static final String MAX_SPEED_COLUMN = "maxSpeed";

    /** Maximum acceleration column id. */
    public static final String MAX_ACCELERATION_COLUMN = "maxAcceleration";

    /** Maximum deceleration column id. */
    public static final String MAX_DECELERATION_COLUMN = "maxDeceleration";

    /** Front column id. */
    public static final String FRONT_COLUMN = "front";

    /** Network. */
    private final Network network;

    /** GTU types per their id. */
    private final ImmutableMap<String, GtuType> gtuTypes;

    /** Strategical planner factory. */
    private final LaneBasedStrategicalPlannerFactory<?> strategicalPlannerFactory;

    /** Critical time-to-collision for GTU placement. */
    private final Duration timeToCollision;

    /** Random number stream. */
    private final StreamInterface stream;

    /** Stored column numbers for present columns in injection table. */
    private final Map<String, Integer> columnNumbers = new LinkedHashMap<>();

    /** Separate iterator to obtain the id, as this is requested asynchronously with the other characteristics. */
    private final Iterator<Row> idIterator;

    /** Separate iterator to obtain the speed, as this is requested asynchronously with the other characteristics. */
    private final Iterator<Row> speedIterator;

    /** Next speed to generate next GTU with. */
    private Speed nextSpeed;

    /** Iterator over all injections. */
    private final Iterator<Row> characteristicsIterator;

    /** Current row with characteristics from injection. */
    private Row characteristicsRow;

    /** Previous arrival time to calculate inter-arrival time. */
    private Duration previousArrival = Duration.ZERO;

    /** Positions per link, lane and position (on lane). */
    private MultiKeyMap<GeneratorLanePosition> lanePositions;

    /** All lane positions, returned as {@code GeneratorPositions}. */
    private Set<GeneratorLanePosition> allLanePositions;

    /** Cached characteristics generator, to always return the same. */
    private LaneBasedGtuCharacteristicsGenerator characteristicsGenerator;

    /** Boolean to check inter-arrival time and characteristics drawing consistency. */
    private boolean readyForCharacteristicsDraw = false;

    /**
     * Constructor. Depending on what information is provided in the injections table, some arguments may or should not be
     * {@code null}. In particular:
     * <ul>
     * <li>"time": always required, allows the {@code Injections} to be used as a {@code Generator<Duration>}.</li>
     * <li>"id": allows the {@code Injections} to be used as a {@code Supplier<String>} for GTU ids.</li>
     * <li>"position", "lane", "link": allows the {@code Injections} to be used as a {@code GeneratorPositions}, requires
     * <b>network</b>.</li>
     * <li>"speed": allows the {@code Injections} to be used as a {@code RoomChecker}, requires <b>timeToCollision</b>.</li>
     * <li><i>all other columns</i>: allows the {@code Injections} to be used as a {@code LaneBasedGtuCharacteristicsGenerator}
     * through {@code asLaneBasedGtuCharacteristicsGenerator()}, requires <b>gtuTypes</b>, <b>network</b>,
     * <b>strategicalPlannerFactory</b> and <b>stream</b>.
     * </ul>
     * Time should be in increasing order. If length is provided, but no front, front will be 75% of the length.
     * @param table Table; table with at least a "time" column.
     * @param network Network; network, may be {@code null}.
     * @param gtuTypes ImmutableMap&lt;String, GtuType&gt;; GTU types, as obtained from {@code Definitions}, may be
     *            {@code null}.
     * @param strategicalPlannerFactory LaneBasedStrategicalPlannerFactory&lt;?&gt;; strategical planner factory, may be
     *            {@code null}.
     * @param stream StreamInterface; random number stream, may be {@code null}.
     * @param timeToCollision Duration; critical time-to-collision to allow GTU generation, may be {@code null}.
     * @throws IllegalArgumentException when the right arguments are not provided for the columns in the injection table.
     */
    public Injections(final Table table, final Network network, final ImmutableMap<String, GtuType> gtuTypes,
            final LaneBasedStrategicalPlannerFactory<?> strategicalPlannerFactory, final StreamInterface stream,
            final Duration timeToCollision) throws IllegalArgumentException
    {
        Throw.whenNull(table, "Table may not be null.");
        Table sortedTable = sortTable(table);
        this.idIterator = sortedTable.iterator();
        this.speedIterator = sortedTable.iterator();
        this.characteristicsIterator = sortedTable.iterator();
        this.network = network;
        this.gtuTypes = gtuTypes == null ? new ImmutableLinkedHashMap<>(Collections.emptyMap()) : gtuTypes;
        this.strategicalPlannerFactory = strategicalPlannerFactory;
        this.timeToCollision = timeToCollision;
        this.stream = stream;

        sortedTable.getColumns().forEach((c) -> this.columnNumbers.put(c.getId(), sortedTable.getColumnNumber(c)));
        boolean needStrategicalPlannerFactory = checkColumnTypesNeedStrategicalPlannerFactory(sortedTable);
        Throw.when(needStrategicalPlannerFactory && (gtuTypes == null || gtuTypes.isEmpty()), IllegalArgumentException.class,
                "Injection table contains columns that require GTU types.");
        Throw.when(needStrategicalPlannerFactory && strategicalPlannerFactory == null, IllegalArgumentException.class,
                "Injection table contains columns that require a strategical planner factory.");
        Throw.when(needStrategicalPlannerFactory && network == null, IllegalArgumentException.class,
                "Injection table contains columns that require a network.");
        Throw.when(needStrategicalPlannerFactory && stream == null, IllegalArgumentException.class,
                "Injection table contains columns that require a stream of random numbers.");
        Throw.when(!this.columnNumbers.containsKey(TIME_COLUMN), IllegalArgumentException.class,
                "Injection table contains no time column.");

        createLanePositions(sortedTable);
    }

    /**
     * Makes sure the table is sorted by the time column.
     * @param table Table; input table.
     * @return Table; table sorted by time column.
     */
    private static Table sortTable(final Table table)
    {
        int timeColumn = table.getColumnNumber(TIME_COLUMN);
        Iterator<Row> iterator = table.iterator();
        Duration prev = iterator.hasNext() ? (Duration) iterator.next().getValue(timeColumn) : null;
        while (iterator.hasNext())
        {
            Duration next = (Duration) iterator.next().getValue(timeColumn);
            if (next.lt(prev))
            {
                // data is not in order
                List<Row> data = new ArrayList<>();
                for (Row row : table)
                {
                    data.add(row);
                }
                Collections.sort(data, new Comparator<Row>()
                {
                    /** {@inheritDoc} */
                    @Override
                    public int compare(final Row o1, final Row o2)
                    {
                        return ((Duration) o1.getValue(timeColumn)).compareTo((Duration) o2.getValue(timeColumn));
                    }
                });
                ListTable out = new ListTable(table.getId(), table.getDescription(), table.getColumns().toList());
                for (Row row : data)
                {
                    out.addRow(row.getValues());
                }
                return out;
            }
            prev = next;
        }
        return table;
    }

    /**
     * Checks whether all columns have the right value type.
     * @param table Table; injection table.
     * @return boolean; whether columns are present that require a strategical planner factory in order to be processed.
     */
    private boolean checkColumnTypesNeedStrategicalPlannerFactory(final Table table)
    {
        boolean needStrategicalPlannerFactory = false;
        for (Entry<String, Integer> entry : this.columnNumbers.entrySet())
        {
            Class<?> needClass;
            switch (entry.getKey())
            {
                case TIME_COLUMN:
                    needClass = Duration.class;
                    break;
                case ID_COLUMN:
                case LANE_COLUMN:
                case LINK_COLUMN:
                    needClass = String.class;
                    break;
                case GTU_TYPE_COLUMN:
                case ORIGIN_COLUMN:
                case DESTINATION_COLUMN:
                case ROUTE_COLUMN:
                    needClass = String.class;
                    needStrategicalPlannerFactory = true;
                    break;
                case SPEED_COLUMN:
                    needClass = Speed.class;
                    break;
                case MAX_SPEED_COLUMN:
                    needClass = Speed.class;
                    needStrategicalPlannerFactory = true;
                    break;
                case POSITION_COLUMN:
                    needClass = Length.class;
                    break;
                case LENGTH_COLUMN:
                case WIDTH_COLUMN:
                case FRONT_COLUMN:
                    needClass = Length.class;
                    needStrategicalPlannerFactory = true;
                    break;
                case MAX_ACCELERATION_COLUMN:
                case MAX_DECELERATION_COLUMN:
                    needClass = Acceleration.class;
                    needStrategicalPlannerFactory = true;
                    break;
                default:
                    Logger.info("Column " + entry.getKey() + " for GTU injection not supported. It is ignored.");
                    needClass = null;
            }
            if (needClass != null)
            {
                Class<?> columnValueClass = table.getColumn(entry.getValue()).getValueType();
                Throw.when(!needClass.isAssignableFrom(columnValueClass), IllegalArgumentException.class,
                        "Column %s has value type %s, but type %s is required.", entry.getKey(), columnValueClass, needClass);
            }
        }
        return needStrategicalPlannerFactory;
    }

    /**
     * Creates all the lane positions for GTU generation.
     * @param table Table; injection table.
     */
    private void createLanePositions(final Table table)
    {
        if (this.columnNumbers.containsKey(POSITION_COLUMN) && this.columnNumbers.containsKey(LANE_COLUMN)
                && this.columnNumbers.containsKey(LINK_COLUMN))
        {
            this.lanePositions = new MultiKeyMap<>(String.class, String.class, Length.class);
            this.allLanePositions = new LinkedHashSet<>();
            for (Row row : table)
            {
                String linkId = (String) row.getValue(this.columnNumbers.get(LINK_COLUMN));
                Link link = this.network.getLink(linkId);
                Throw.when(link == null, IllegalArgumentException.class, "Link %s in injections is not in the network.",
                        linkId);
                Throw.when(!(link instanceof CrossSectionLink), IllegalArgumentException.class,
                        "Injection table contains link that is not a CrossSectionLink.");

                String laneId = (String) row.getValue(this.columnNumbers.get(LANE_COLUMN));
                // get and sort lanes to get the lane number (1 = right-most lane)
                List<Lane> lanes = ((CrossSectionLink) link).getLanes();
                Collections.sort(lanes, new Comparator<Lane>()
                {
                    /** {@inheritDoc} */
                    @Override
                    public int compare(final Lane o1, final Lane o2)
                    {
                        return o1.getOffsetAtBegin().compareTo(o2.getOffsetAtBegin());
                    }
                });
                int laneNumber = 0;
                for (int i = 0; i < lanes.size(); i++)
                {
                    if (lanes.get(i).getId().equals(laneId))
                    {
                        laneNumber = i + 1;
                        break;
                    }
                }
                Throw.when(laneNumber == 0, IllegalArgumentException.class,
                        "Injection table contains lane %s on link %s, but the link has no such lane.", laneId, linkId);

                Length position = (Length) row.getValue(this.columnNumbers.get(POSITION_COLUMN));
                Throw.when(position.lt0() || position.gt(lanes.get(laneNumber - 1).getLength()), IllegalArgumentException.class,
                        "Injection table contains position %s on lane %s on link %s, but the position is negative or "
                                + "beyond the length of the lane.",
                        position, laneId, linkId);

                GeneratorLanePosition generatorLanePosition = new GeneratorLanePosition(laneNumber,
                        new LanePosition(lanes.get(laneNumber - 1), position), (CrossSectionLink) link);
                if (this.allLanePositions.add(generatorLanePosition))
                {
                    this.lanePositions.put(generatorLanePosition, linkId, laneId, position);
                }
            }
        }
        else if (this.columnNumbers.containsKey(POSITION_COLUMN) || this.columnNumbers.containsKey(LANE_COLUMN)
                || this.columnNumbers.containsKey(LINK_COLUMN))
        {
            // only partial information is provided (if none, we assume the user intends to use an external fixed position)
            // as the user may still use another source for generator positions, we do not throw an exception
            Logger.info("For injections to be used as GeneratorPositions, define a link, lane and position (on lane) column.");
        }
    }

    /** {@inheritDoc} */
    @Override
    public String get()
    {
        // This method implements Supplier<String> as an id generator.
        Throw.when(!this.idIterator.hasNext(), NoSuchElementException.class, "No more ids to draw.");
        Throw.when(!this.columnNumbers.containsKey(ID_COLUMN), IllegalStateException.class,
                "Using Injections as id generator, but the injection table has no id column.");
        return (String) this.idIterator.next().getValue(this.columnNumbers.get(ID_COLUMN));
    }

    /**
     * Returns whether the column of given id is present.
     * @param columnId String; column id.
     * @return boolean; whether the column of given id is present.
     */
    public boolean hasColumn(final String columnId)
    {
        return this.columnNumbers.containsKey(columnId);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized Duration draw() throws ProbabilityException, ParameterException
    {
        if (!this.characteristicsIterator.hasNext())
        {
            return null; // stops LaneBasedGtuGenerator
        }
        this.characteristicsRow = this.characteristicsIterator.next();
        this.readyForCharacteristicsDraw = true;
        Duration t = (Duration) getCharacteristic(TIME_COLUMN);
        Throw.when(t.lt(this.previousArrival), IllegalStateException.class, "Arrival times in injection not increasing.");
        Duration interArrivalTime = t.minus(this.previousArrival);
        this.previousArrival = t;
        return interArrivalTime;
    }

    /**
     * Returns a characteristics generator view of the injections, as used by {@code LaneBasedGtuGenerator}. This requires at
     * the least that a GTU type column, a strategical planner factory, a network, and a stream of random numbers are provided.
     * @return LaneBasedGtuCharacteristicsGenerator; characteristics generator view of the injections.
     */
    public LaneBasedGtuCharacteristicsGenerator asLaneBasedGtuCharacteristicsGenerator()
    {
        if (this.characteristicsGenerator == null)
        {
            Throw.when(!this.columnNumbers.containsKey(GTU_TYPE_COLUMN), IllegalStateException.class,
                    "A GTU type column is required for generation of characteristics.");

            this.characteristicsGenerator = new LaneBasedGtuCharacteristicsGenerator()
            {
                /** Default characteristics, generated as needed. */
                private GtuCharacteristics defaultCharacteristics;

                /** {@inheritDoc} */
                @Override
                public LaneBasedGtuCharacteristics draw() throws ProbabilityException, ParameterException, GtuException
                {
                    synchronized (Injections.this)
                    {
                        Throw.when(Injections.this.characteristicsRow == null, IllegalStateException.class,
                                "Must draw inter-arrival time before drawing GTU characteristics.");
                        Throw.when(!Injections.this.readyForCharacteristicsDraw, IllegalStateException.class,
                                "Should not draw GTU characteristics again before inter-arrival time was drawn in between.");
                        Injections.this.readyForCharacteristicsDraw = false;
                        GtuType gtuType = Injections.this.gtuTypes.get((String) getCharacteristic(GTU_TYPE_COLUMN));

                        Length length = (Length) assureCharacteristic(LENGTH_COLUMN, gtuType, (g) -> g.getLength());
                        Length width = (Length) assureCharacteristic(WIDTH_COLUMN, gtuType, (g) -> g.getWidth());
                        Speed maxSpeed = (Speed) assureCharacteristic(MAX_SPEED_COLUMN, gtuType, (g) -> g.getMaximumSpeed());
                        Acceleration maxAcceleration = (Acceleration) assureCharacteristic(MAX_ACCELERATION_COLUMN, gtuType,
                                (g) -> g.getMaximumAcceleration());
                        Acceleration maxDeceleration = (Acceleration) assureCharacteristic(MAX_DECELERATION_COLUMN, gtuType,
                                (g) -> g.getMaximumDeceleration());
                        this.defaultCharacteristics = null; // reset for next draw
                        Length front = Injections.this.columnNumbers.containsKey(FRONT_COLUMN)
                                ? (Length) getCharacteristic(FRONT_COLUMN) : length.times(0.75);
                        GtuCharacteristics characteristics = new GtuCharacteristics(gtuType, length, width, maxSpeed,
                                maxAcceleration, maxDeceleration, front);

                        Route route = Injections.this.columnNumbers.containsKey(ROUTE_COLUMN)
                                ? (Route) Injections.this.network.getRoute((String) getCharacteristic(ROUTE_COLUMN)) : null;
                        Node origin = Injections.this.columnNumbers.containsKey(ORIGIN_COLUMN)
                                ? (Node) Injections.this.network.getNode((String) getCharacteristic(ORIGIN_COLUMN)) : null;
                        Node destination = Injections.this.columnNumbers.containsKey(DESTINATION_COLUMN)
                                ? (Node) Injections.this.network.getNode((String) getCharacteristic(DESTINATION_COLUMN)) : null;
                        return new LaneBasedGtuCharacteristics(characteristics, Injections.this.strategicalPlannerFactory,
                                route, origin, destination, VehicleModel.MINMAX);
                    }
                }

                /**
                 * Tries to obtain a column value. If it is not provided, takes the value from generated default
                 * characteristics.
                 * @param column String; characteristic column name.
                 * @param gtuType GtuType; GTU type of the GTU to be generated.
                 * @param supplier Function&lt;GtuCharacteristics, ?&gt;; takes value from default characteristics.
                 * @return Object; object value for the characteristic.
                 * @throws GtuException; if there are no default characteristics for the GTU type, but these are required.
                 */
                private Object assureCharacteristic(final String column, final GtuType gtuType,
                        final Function<GtuCharacteristics, ?> supplier) throws GtuException
                {
                    if (Injections.this.columnNumbers.containsKey(column))
                    {
                        return getCharacteristic(column);
                    }
                    if (this.defaultCharacteristics == null)
                    {
                        this.defaultCharacteristics =
                                GtuType.defaultCharacteristics(gtuType, Injections.this.network, Injections.this.stream);
                    }
                    return supplier.apply(this.defaultCharacteristics);
                }
            };
        }
        return this.characteristicsGenerator;
    }

    /** {@inheritDoc} */
    @Override
    public GeneratorLanePosition draw(final GtuType gtuType, final LaneBasedGtuCharacteristics characteristics,
            final Map<CrossSectionLink, Map<Integer, Integer>> unplaced) throws GtuException
    {
        Throw.when(this.lanePositions == null, IllegalStateException.class,
                "Injection table without position, lane and link column cannot be used to draw generator positions.");
        String link = (String) getCharacteristic(LINK_COLUMN);
        String lane = (String) getCharacteristic(LANE_COLUMN);
        Length position = (Length) getCharacteristic(POSITION_COLUMN);
        return this.lanePositions.get(link, lane, position);
    }

    /** {@inheritDoc} */
    @Override
    public Set<GeneratorLanePosition> getAllPositions()
    {
        Throw.when(this.lanePositions == null, IllegalStateException.class,
                "Injection table without position, lane and link column cannot be used to draw generator positions.");
        return this.allLanePositions;
    }

    /**
     * Returns placement for injected GTUs, as used by {@code LaneBasedGtuGenerator}. This needs speed to be provided in the
     * injections, and a minimum time-to-collision value. Besides the time-to-collision value, the minimum headway for a
     * successful placement is t*v + 3m, where t = 1s and v the generation speed.
     * @param leaders SortedSet&lt;HeadwayGtu&gt;; leaders, usually 1, possibly more after a branch
     * @param characteristics LaneBasedGtuCharacteristics; characteristics of the proposed new GTU
     * @param since Duration; time since the GTU wanted to arrive
     * @param initialPosition LanePosition; initial position
     * @return Speed; maximum safe speed, or null if a GTU with the specified characteristics cannot be placed at the current
     *         time
     * @throws NetworkException this method may throw a NetworkException if it encounters an error in the network structure
     * @throws GtuException on parameter exception
     */
    @Override
    public Placement canPlace(final SortedSet<HeadwayGtu> leaders, final LaneBasedGtuCharacteristics characteristics,
            final Duration since, final LanePosition initialPosition) throws NetworkException, GtuException
    {
        Throw.when(!this.columnNumbers.containsKey(SPEED_COLUMN), IllegalStateException.class,
                "Injection table without speed cannot be used to determine a GTU placement.");
        Throw.when(this.timeToCollision == null, IllegalStateException.class,
                "Injections used to place GTUs, but no acceptable time-to-collision is provided.");
        if (this.nextSpeed == null)
        {
            Throw.when(!this.speedIterator.hasNext(), NoSuchElementException.class, "No more speed to draw.");
            this.nextSpeed = (Speed) this.speedIterator.next().getValue(this.columnNumbers.get(SPEED_COLUMN));
        }
        if (leaders.isEmpty())
        {
            // no leaders: free
            Placement placement = new Placement(this.nextSpeed, initialPosition);
            this.nextSpeed = null;
            return placement;
        }
        HeadwayGtu leader = leaders.first();
        if ((this.nextSpeed.le(leader.getSpeed())
                || leader.getDistance().divide(this.nextSpeed.minus(leader.getSpeed())).gt(this.timeToCollision))
                && leader.getDistance()
                        .gt(this.nextSpeed.times(new Duration(1.0, DurationUnit.SI)).plus(new Length(3.0, LengthUnit.SI))))
        {
            Placement placement = new Placement(this.nextSpeed, initialPosition);
            this.nextSpeed = null;
            return placement;
        }
        return Placement.NO;
    }

    /**
     * Shorthand to retrieve a column value from the current characteristics row.
     * @param column String; characteristic column name.
     * @return Object; object value for the characteristic.
     */
    private Object getCharacteristic(final String column)
    {
        return this.characteristicsRow.getValue(this.columnNumbers.get(column));
    }

}