Query.java

package org.opentrafficsim.kpi.sampling;

import java.util.ArrayList;
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.Set;
import java.util.UUID;

import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Frequency;
import org.djunits.value.vdouble.scalar.Length;
import org.djunits.value.vdouble.scalar.Time;
import org.djutils.base.Identifiable;
import org.djutils.exceptions.Throw;
import org.djutils.immutablecollections.ImmutableIterator;
import org.opentrafficsim.kpi.interfaces.GtuData;
import org.opentrafficsim.kpi.interfaces.LaneData;
import org.opentrafficsim.kpi.interfaces.LinkData;
import org.opentrafficsim.kpi.sampling.filter.FilterDataSet;
import org.opentrafficsim.kpi.sampling.filter.FilterDataType;

/**
 * A query defines which subset of trajectory information should be included. This is in terms of space-time regions, and in
 * terms of filter data of trajectories, e.g. only include trajectories of trucks.
 * <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 final class Query<G extends GtuData, L extends LaneData<L>> implements Identifiable
{

    /** unique id. */
    private final String id;

    /** Sampling. */
    private final Sampler<G, L> sampler;

    /** Description. */
    private final String description;

    /** Filter data set. */
    private final FilterDataSet filterDataSet;

    /** Update frequency. */
    private final Frequency updateFrequency;

    /** Interval to gather statistics over. */
    private final Duration interval;

    /** List of space-time regions of this query. */
    private final List<SpaceTimeRegion<? extends L>> spaceTimeRegions = new ArrayList<>();

    /**
     * Constructor.
     * @param sampler sampler
     * @param id id
     * @param description description
     * @param filterDataSet filter data
     * @throws NullPointerException if sampling, description or filterDataSet is null
     */
    public Query(final Sampler<G, L> sampler, final String id, final String description, final FilterDataSet filterDataSet)
    {
        this(sampler, id, description, filterDataSet, null, null);
    }

    /**
     * Constructor with time interval and update frequency.
     * @param sampler sampler
     * @param id id, may be {@code null}
     * @param description description
     * @param filterDataSet filter data
     * @param updateFrequency update frequency, used by external controller, may be {@code null}
     * @param interval interval to gather statistics over, used by external controller, may be {@code null}
     * @throws NullPointerException if sampling, description or filterDataSet is {@code null}
     */
    public Query(final Sampler<G, L> sampler, final String id, final String description, final FilterDataSet filterDataSet,
            final Frequency updateFrequency, final Duration interval)
    {
        Throw.whenNull(sampler, "Sampling may not be null.");
        Throw.whenNull(description, "Description may not be null.");
        Throw.whenNull(filterDataSet, "Meta data may not be null.");
        this.sampler = sampler;
        this.filterDataSet = new FilterDataSet(filterDataSet);
        this.id = id == null ? UUID.randomUUID().toString() : id;
        this.description = description;
        this.updateFrequency = updateFrequency;
        this.interval = interval;
    }

    @Override
    public String getId()
    {
        return this.id.toString();
    }

    /**
     * Returns the description.
     * @return description
     */
    public String getDescription()
    {
        return this.description;
    }

    /**
     * Returns the update frequency.
     * @return updateFrequency.
     */
    public Frequency getUpdateFrequency()
    {
        return this.updateFrequency;
    }

    /**
     * Returns the time interval.
     * @return interval.
     */
    public Duration getInterval()
    {
        return this.interval;
    }

    /**
     * Returns the number of filter datas.
     * @return number of filter data entries
     */
    public int filterSize()
    {
        return this.filterDataSet.size();
    }

    /**
     * Returns an iterator over the filter datas and the related data sets.
     * @return iterator over filter data entries, removal is not allowed
     */
    public Iterator<Entry<FilterDataType<?, ?>, Set<?>>> getFilterDataSetIterator()
    {
        return this.filterDataSet.getFilterDataSetIterator();
    }

    /**
     * Defines a region in space and time for which this query is valid. All lanes in the link are included.
     * @param link link
     * @param startPosition start position
     * @param endPosition end position
     * @param startTime start time
     * @param endTime end time
     */
    public void addSpaceTimeRegionLink(final LinkData<? extends L> link, final Length startPosition, final Length endPosition,
            final Time startTime, final Time endTime)
    {
        Throw.whenNull(link, "Link may not be null.");
        Throw.whenNull(startPosition, "Start position may not be null.");
        Throw.whenNull(endPosition, "End position may not be null.");
        Throw.whenNull(startTime, "Start time may not be null.");
        Throw.whenNull(endTime, "End time may not be null.");
        Throw.when(endPosition.lt(startPosition), IllegalArgumentException.class,
                "End position should be greater than start position.");
        Throw.when(endTime.lt(startTime), IllegalArgumentException.class, "End time should be greater than start time.");
        double fStart = startPosition.si / link.getLength().si;
        double fEnd = endPosition.si / link.getLength().si;
        for (L lane : link.getLanes())
        {
            Length x0 = lane.getLength().times(fStart);
            Length x1 = lane.getLength().times(fEnd);
            addSpaceTimeRegion(lane, x0, x1, startTime, endTime);
        }
    }

    /**
     * Defines a region in space and time for which this query is valid.
     * @param lane lane
     * @param startPosition start position
     * @param endPosition end position
     * @param startTime start time
     * @param endTime end time
     */
    public void addSpaceTimeRegion(final L lane, final Length startPosition, final Length endPosition, final Time startTime,
            final Time endTime)
    {
        Throw.whenNull(lane, "Lane direction may not be null.");
        Throw.whenNull(startPosition, "Start position may not be null.");
        Throw.whenNull(endPosition, "End position may not be null.");
        Throw.whenNull(startTime, "Start time may not be null.");
        Throw.whenNull(endTime, "End time may not be null.");
        Throw.when(endPosition.lt(startPosition), IllegalArgumentException.class,
                "End position should be greater than start position.");
        Throw.when(endTime.lt(startTime), IllegalArgumentException.class, "End time should be greater than start time.");
        SpaceTimeRegion<L> spaceTimeRegion = new SpaceTimeRegion<>(lane, startPosition, endPosition, startTime, endTime);
        this.sampler.registerSpaceTimeRegion(spaceTimeRegion);
        this.spaceTimeRegions.add(spaceTimeRegion);
    }

    /**
     * Returns the number of space-time regions.
     * @return number of space-time regions
     */
    public int spaceTimeRegionSize()
    {
        return this.spaceTimeRegions.size();
    }

    /**
     * Returns an iterator over the space-time regions.
     * @return iterator over space-time regions, removal is not allowed
     */
    public Iterator<SpaceTimeRegion<? extends L>> getSpaceTimeIterator()
    {
        return new ImmutableIterator<>(this.spaceTimeRegions.iterator());
    }

    /**
     * Returns a list of TrajectoryGroups in accordance with the query. Each {@code TrajectoryGroup} contains {@code Trajectory}
     * objects pertaining to a {@code SpaceTimeRegion} from the query. A {@code Trajectory} is only included if all the filter
     * data of this query accepts the trajectory. This method uses {@code Time.ZERO} as start.
     * @param endTime end time of interval to get trajectory groups for
     * @param <T> underlying class of filter data type and its value
     * @return list of trajectory groups in accordance with the query
     */
    public <T> List<TrajectoryGroup<G>> getTrajectoryGroups(final Time endTime)
    {
        return getTrajectoryGroups(Time.ZERO, endTime);
    }

    /**
     * Returns a list of TrajectoryGroups in accordance with the query. Each {@code TrajectoryGroup} contains {@code Trajectory}
     * objects pertaining to a {@code SpaceTimeRegion} from the query. A {@code Trajectory} is only included if all the filter
     * data of this query accepts the trajectory.
     * @param startTime start time of interval to get trajectory groups for
     * @param endTime start time of interval to get trajectory groups for
     * @param <T> underlying class of filter data type and its value
     * @return list of trajectory groups in accordance with the query
     */
    @SuppressWarnings("unchecked")
    public <T> List<TrajectoryGroup<G>> getTrajectoryGroups(final Time startTime, final Time endTime)
    {
        Throw.whenNull(startTime, "Start t may not be null.");
        Throw.whenNull(endTime, "End t may not be null.");
        // Step 1) gather trajectories per GTU, truncated over space and time
        Map<String, TrajectoryAcceptList> trajectoryAcceptLists = new LinkedHashMap<>();
        List<TrajectoryGroup<G>> trajectoryGroupList = new ArrayList<>();
        for (SpaceTimeRegion<? extends L> spaceTimeRegion : this.spaceTimeRegions)
        {
            Time start = startTime.gt(spaceTimeRegion.startTime()) ? startTime : spaceTimeRegion.startTime();
            Time end = endTime.lt(spaceTimeRegion.endTime()) ? endTime : spaceTimeRegion.endTime();
            TrajectoryGroup<G> trajectoryGroup;
            if (this.sampler.getSamplerData().getTrajectoryGroup(spaceTimeRegion.lane()) == null)
            {
                trajectoryGroup = new TrajectoryGroup<>(start, spaceTimeRegion.lane());
            }
            else
            {
                trajectoryGroup = this.sampler.getSamplerData().getTrajectoryGroup(spaceTimeRegion.lane())
                        .getTrajectoryGroup(spaceTimeRegion.startPosition(), spaceTimeRegion.endPosition(), start, end);
            }
            for (Trajectory<G> trajectory : trajectoryGroup.getTrajectories())
            {
                if (!trajectoryAcceptLists.containsKey(trajectory.getGtuId()))
                {
                    trajectoryAcceptLists.put(trajectory.getGtuId(), new TrajectoryAcceptList());
                }
                trajectoryAcceptLists.get(trajectory.getGtuId()).addTrajectory(trajectory, trajectoryGroup);
            }
            trajectoryGroupList.add(trajectoryGroup);
        }
        // Step 2) accept per GTU
        Iterator<String> iterator = trajectoryAcceptLists.keySet().iterator();
        while (iterator.hasNext())
        {
            String gtuId = iterator.next();
            TrajectoryAcceptList trajectoryAcceptListCombined = trajectoryAcceptLists.get(gtuId);
            trajectoryAcceptListCombined.acceptAll(); // refuse only if any filter data type refuses
            for (FilterDataType<?, ?> filterDataType : this.filterDataSet.getFilterDataTypes())
            {
                // create safe copy per filter data type, with defaults accepts = false
                TrajectoryAcceptList trajectoryAcceptListCopy = copyTrajectoryAcceptList(trajectoryAcceptLists.get(gtuId));
                // request filter data type to accept or reject
                ((FilterDataType<T, ?>) filterDataType).accept(trajectoryAcceptListCopy,
                        (Set<T>) new LinkedHashSet<>(this.filterDataSet.get(filterDataType)));
                // combine acceptance/rejection of filter data types so far
                for (int i = 0; i < trajectoryAcceptListCopy.size(); i++)
                {
                    Trajectory<?> trajectory = trajectoryAcceptListCopy.getTrajectory(i);
                    trajectoryAcceptListCombined.acceptTrajectory(trajectory,
                            trajectoryAcceptListCombined.isAccepted(trajectory)
                                    && trajectoryAcceptListCopy.isAccepted(trajectory));
                }
            }
        }
        // Step 3) filter trajectories
        List<TrajectoryGroup<G>> out = new ArrayList<>();
        for (TrajectoryGroup<G> full : trajectoryGroupList)
        {
            TrajectoryGroup<G> filtered = new TrajectoryGroup<>(full.getStartTime(), full.getLane());
            for (Trajectory<G> trajectory : full.getTrajectories())
            {
                String gtuId = trajectory.getGtuId();
                if (trajectory.size() > 0 && trajectoryAcceptLists.get(gtuId).isAccepted(trajectory))
                {
                    filtered.addTrajectory(trajectory);
                }
            }
            out.add(filtered);
        }
        return out;
    }

    /**
     * Returns a copy of the trajectory accept list, with all assumed not accepted.
     * @param trajectoryAcceptList trajectory accept list to copy
     * @return copy of the trajectory accept list, with all assumed not accepted
     */
    private TrajectoryAcceptList copyTrajectoryAcceptList(final TrajectoryAcceptList trajectoryAcceptList)
    {
        TrajectoryAcceptList trajectoryAcceptListCopy = new TrajectoryAcceptList();
        for (int i = 0; i < trajectoryAcceptList.size(); i++)
        {
            trajectoryAcceptListCopy.addTrajectory(trajectoryAcceptList.getTrajectory(i),
                    trajectoryAcceptList.getTrajectoryGroup(i));
        }
        return trajectoryAcceptListCopy;
    }

    /**
     * Returns the sampler.
     * @return sampler.
     */
    public Sampler<G, L> getSampler()
    {
        return this.sampler;
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + this.description.hashCode();
        result = prime * result + ((this.interval == null) ? 0 : this.interval.hashCode());
        result = prime * result + this.filterDataSet.hashCode();
        result = prime * result + this.sampler.hashCode();
        result = prime * result + this.spaceTimeRegions.hashCode();
        result = prime * result + this.id.hashCode();
        result = prime * result + ((this.updateFrequency == null) ? 0 : this.updateFrequency.hashCode());
        return result;
    }

    @Override
    public boolean equals(final Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (obj == null)
        {
            return false;
        }
        if (getClass() != obj.getClass())
        {
            return false;
        }
        Query<?, ?> other = (Query<?, ?>) obj;
        if (!this.description.equals(other.description))
        {
            return false;
        }
        if (this.interval == null)
        {
            if (other.interval != null)
            {
                return false;
            }
        }
        else if (!this.interval.equals(other.interval))
        {
            return false;
        }
        if (!this.filterDataSet.equals(other.filterDataSet))
        {
            return false;
        }
        if (!this.sampler.equals(other.sampler))
        {
            return false;
        }
        if (!this.spaceTimeRegions.equals(other.spaceTimeRegions))
        {
            return false;
        }
        if (!this.id.equals(other.id))
        {
            return false;
        }
        if (this.updateFrequency == null)
        {
            if (other.updateFrequency != null)
            {
                return false;
            }
        }
        else if (!this.updateFrequency.equals(other.updateFrequency))
        {
            return false;
        }
        return true;
    }

    @Override
    public String toString()
    {
        return "Query (" + this.description + ")";
    }

}