Sampler.java

package org.opentrafficsim.kpi.sampling;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.djunits.value.vdouble.scalar.Acceleration;
import org.djunits.value.vdouble.scalar.Length;
import org.djunits.value.vdouble.scalar.Speed;
import org.djunits.value.vdouble.scalar.Time;
import org.djutils.exceptions.Throw;
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;

/**
 * Sampler is the highest level organizer for sampling.
 * <p>
 * Copyright (c) 2013-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://github.com/peter-knoppers">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 * @param <G> GTU data type
 * @param <L> lane data type
 */
public abstract class Sampler<G extends GtuData, L extends LaneData<L>>
{

    /** Sampler data. */
    private final SamplerData<G> samplerData;

    /** Registration of included extended data types. */
    private final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedDataTypes;

    /** Set of registered filter data types. */
    private final Set<FilterDataType<?, ? super G>> filterDataTypes;

    /** Registration of current trajectories of each GTU per lane. */
    private final Map<String, Map<L, Trajectory<G>>> trajectoryPerGtu = new LinkedHashMap<>();

    /** Number of space time regions that currently need data (i.e. overlap counter). */
    private final Map<L, Integer> currentlyRecording = new LinkedHashMap<>();

    /**
     * Constructor.
     * @param extendedDataTypes extended data types.
     * @param filterDataTypes filter data types.
     */
    public Sampler(final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedDataTypes,
            final Set<FilterDataType<?, ? super G>> filterDataTypes)
    {
        this.extendedDataTypes = new LinkedHashSet<>(extendedDataTypes);
        this.filterDataTypes = new LinkedHashSet<>(filterDataTypes);
        this.samplerData = new SamplerData<>(extendedDataTypes, filterDataTypes);
    }

    /**
     * Underlying sampler data.
     * @return underlying sampler data
     */
    public SamplerData<G> getSamplerData()
    {
        return this.samplerData;
    }

    /**
     * Whether this sampler has the given extended data type registered to it.
     * @param extendedDataType extended data type
     * @return whether this sampler has the given extended data type registered to it
     */
    public boolean contains(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
    {
        return this.extendedDataTypes.contains(extendedDataType);
    }

    /**
     * Registers a space-time region. Data will be recorded across the entire length of a lane, but only during specified time
     * periods.
     * @param spaceTimeRegion space-time region
     * @throws IllegalStateException if data is not available from the requested start time
     */
    public final void registerSpaceTimeRegion(final SpaceTimeRegion<L> spaceTimeRegion)
    {
        Throw.whenNull(spaceTimeRegion, "SpaceTimeRegion may not be null.");
        Time firstPossibleDataTime;
        if (this.samplerData.contains(spaceTimeRegion.lane()))
        {
            firstPossibleDataTime = this.samplerData.getTrajectoryGroup(spaceTimeRegion.lane()).getStartTime();
        }
        else
        {
            firstPossibleDataTime = now();
        }
        Throw.when(spaceTimeRegion.startTime().lt(firstPossibleDataTime), IllegalStateException.class,
                "Space time region with start time %s is defined while data is available from %s onwards.",
                spaceTimeRegion.startTime(), firstPossibleDataTime);
        scheduleStartRecording(spaceTimeRegion.startTime(), spaceTimeRegion.lane());
        scheduleStopRecording(spaceTimeRegion.endTime(), spaceTimeRegion.lane());
    }

    /**
     * Returns the current simulation time.
     * @return current simulation time
     */
    public abstract Time now();

    /**
     * Schedules the start of recording for a given lane, i.e. the implementation has to invoke {@code startRecording} at the
     * specified time, with the given lane as input. In case multiple space time-regions are registered for the same lane, this
     * method is invoked whenever the next space-time region that is added has an earlier start time than any before.
     * @param time time to start recording
     * @param lane lane to start recording
     */
    public abstract void scheduleStartRecording(Time time, L lane);

    /**
     * Schedules the stop of recording for a given lane, i.e. the implementation has to invoke {@code stopRecording} at the
     * specified time, with the given lane as input. In case multiple space time-regions are registered for the same lane, this
     * method is invoked whenever the next space-time region that is added has a later end time than any before.
     * @param time time to stop recording
     * @param lane lane to stop recording
     */
    public abstract void scheduleStopRecording(Time time, L lane);

    /**
     * Start recording at the given time (which should be the current time) on the given lane.
     * @param lane lane
     */
    public final void startRecording(final L lane)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        if (this.currentlyRecording.containsKey(lane))
        {
            this.currentlyRecording.compute(lane, (l, i) -> i + 1);
            return;
        }
        this.currentlyRecording.put(lane, 0);
        if (!this.samplerData.contains(lane))
        {
            this.samplerData.putTrajectoryGroup(lane, new TrajectoryGroup<>(now(), lane));
        }
        initRecording(lane);
    }

    /**
     * Adds listeners to start recording.
     * @param lane lane to initialize recording for
     */
    public abstract void initRecording(L lane);

    /**
     * Stop recording at given lane.
     * @param lane lane
     */
    public final void stopRecording(final L lane)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        if (!this.currentlyRecording.containsKey(lane))
        {
            return; // wrong invocation; ignore
        }
        if (this.currentlyRecording.get(lane) > 0)
        {
            this.currentlyRecording.compute(lane, (l, i) -> i - 1);
            return;
        }
        this.currentlyRecording.remove(lane);
        finalizeRecording(lane);
    }

    /**
     * Remove listeners to stop recording.
     * @param lane lane
     */
    public abstract void finalizeRecording(L lane);

    /**
     * Creates a trajectory with the current snapshot of a GTU.
     * @param lane lane the gtu is at
     * @param position position of the gtu on the lane
     * @param speed speed of the gtu
     * @param acceleration acceleration of the gtu
     * @param time current time
     * @param gtu gtu
     */
    public final void addGtuWithSnapshot(final L lane, final Length position, final Speed speed,
            final Acceleration acceleration, final Time time, final G gtu)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        Throw.whenNull(position, "Position may not be null.");
        if (lane.getLength().lt(position))
        {
            // ignore event if beyond lane length (may happen during lane change)
            return;
        }
        addGtu(lane, gtu);
        snapshot(lane, position, speed, acceleration, time, gtu);
    }

    /**
     * Creates a trajectory, including filter data.
     * @param lane lane the gtu is at
     * @param gtu gtu
     */
    public final void addGtu(final L lane, final G gtu)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        Throw.whenNull(gtu, "GtuData may not be null.");
        String gtuId = gtu.getId();
        Trajectory<G> trajectory = new Trajectory<G>(gtu, makeFilterData(gtu), this.extendedDataTypes);
        this.trajectoryPerGtu.computeIfAbsent(gtuId, (key) -> new LinkedHashMap<>()).put(lane, trajectory);
        this.samplerData.getTrajectoryGroup(lane).addTrajectory(trajectory);
    }

    /**
     * Adds a new snapshot of a GTU to its recording trajectory, if recorded. This method may be invoked on GTUs that are not
     * being recorded; the event will then be ignored.
     * @param lane lane the gtu is at
     * @param position position of the gtu on the lane
     * @param speed speed of the gtu
     * @param acceleration acceleration of the gtu
     * @param time current time
     * @param gtu gtu
     */
    public final void snapshot(final L lane, final Length position, final Speed speed, final Acceleration acceleration,
            final Time time, final G gtu)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        Throw.whenNull(position, "Position may not be null.");
        Throw.whenNull(speed, "Speed may not be null.");
        Throw.whenNull(acceleration, "Acceleration may not be null.");
        Throw.whenNull(time, "Time may not be null.");
        Throw.whenNull(gtu, "GtuData may not be null.");
        String gtuId = gtu.getId();
        Map<L, Trajectory<G>> trajectoryPerLane = this.trajectoryPerGtu.get(gtuId);
        if (trajectoryPerLane != null)
        {
            Trajectory<G> trajectory = trajectoryPerLane.get(lane);
            if (trajectory != null)
            {
                trajectory.add(position, speed, acceleration, time, gtu);
            }
        }
    }

    /**
     * Finalizes a trajectory with the current snapshot of a GTU.
     * @param lane lane the gtu is at
     * @param position position of the gtu on the lane
     * @param speed speed of the gtu
     * @param acceleration acceleration of the gtu
     * @param time current time
     * @param gtu gtu
     */
    public final void removeGtuWithSnapshot(final L lane, final Length position, final Speed speed,
            final Acceleration acceleration, final Time time, final G gtu)
    {
        snapshot(lane, position, speed, acceleration, time, gtu);
        removeGtu(lane, gtu);
    }

    /**
     * Finalizes a trajectory.
     * @param lane lane the gtu is at
     * @param gtu gtu
     */
    public final void removeGtu(final L lane, final G gtu)
    {
        Throw.whenNull(lane, "LaneData may not be null.");
        Throw.whenNull(gtu, "GtuData may not be null.");
        String gtuId = gtu.getId();
        Map<L, Trajectory<G>> trajectoryPerLane = this.trajectoryPerGtu.get(gtuId);
        if (trajectoryPerLane != null)
        {
            trajectoryPerLane.remove(lane);
            if (trajectoryPerLane.isEmpty())
            {
                this.trajectoryPerGtu.remove(gtuId);
            }
        }
    }

    /**
     * Gathers the filter data for filter data types.
     * @param gtu gtu to return filter data for a GTU
     * @return filter data for the given gtu
     */
    private Map<FilterDataType<?, ? super G>, Object> makeFilterData(final G gtu)
    {
        Map<FilterDataType<?, ? super G>, Object> filterData = new LinkedHashMap<>();
        this.filterDataTypes.forEach((filterDataType) -> filterData.put(filterDataType, filterDataType.getValue(gtu)));
        return filterData;
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(this.extendedDataTypes, this.filterDataTypes);
    }

    @Override
    public boolean equals(final Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (obj == null)
        {
            return false;
        }
        if (getClass() != obj.getClass())
        {
            return false;
        }
        Sampler<?, ?> other = (Sampler<?, ?>) obj;
        return Objects.equals(this.extendedDataTypes, other.extendedDataTypes)
                && Objects.equals(this.filterDataTypes, other.filterDataTypes);
    }

}