LoopDetector.java
package org.opentrafficsim.road.network.lane.object.detector;
import java.util.ArrayList;
import java.util.Collection;
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.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.djunits.unit.SpeedUnit;
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.Speed;
import org.djunits.value.vdouble.scalar.Time;
import org.djutils.data.Column;
import org.djutils.data.Row;
import org.djutils.data.Table;
import org.djutils.event.EventType;
import org.djutils.exceptions.Throw;
import org.djutils.exceptions.Try;
import org.djutils.metadata.MetaData;
import org.djutils.metadata.ObjectDescriptor;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.gtu.RelativePosition;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
import org.opentrafficsim.road.network.RoadNetwork;
import org.opentrafficsim.road.network.lane.Lane;
/**
* Detector, measuring a dynamic set of measurements typical for single- or dual-loop detectors. A subsidiary detector is placed
* at a position downstream based on the detector length, that triggers when the rear leaves the detector.
* <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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public class LoopDetector extends LaneDetector
{
/** */
private static final long serialVersionUID = 20180312L;
/** Trigger event. Payload: [Id of LaneBasedGtu]. */
public static final EventType LOOP_DETECTOR_TRIGGERED =
new EventType("LOOPDETECTOR.TRIGGER", new MetaData("Dual loop detector triggered", "Dual loop detector triggered",
new ObjectDescriptor[] {new ObjectDescriptor("Id of GTU", "Id of GTU", String.class)}));
/** Aggregation event. Payload: [Frequency, measurement...]/ */
public static final EventType LOOP_DETECTOR_AGGREGATE = new EventType("LOOPDETECTOR.AGGREGATE", MetaData.NO_META_DATA);
/** Mean speed measurement. */
public static final LoopDetectorMeasurement<Double, Speed> MEAN_SPEED = new LoopDetectorMeasurement<Double, Speed>()
{
@Override
public Double identity()
{
return 0.0;
}
@Override
public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
return cumulative + gtu.getSpeed().si;
}
@Override
public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
return cumulative;
}
@Override
public boolean isPeriodic()
{
return true;
}
@Override
public Speed aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
{
return new Speed(3.6 * cumulative / gtuCount, SpeedUnit.KM_PER_HOUR);
}
@Override
public String getName()
{
return "v";
}
@Override
public String toString()
{
return getName();
}
@Override
public String getDescription()
{
return "mean speed";
}
@Override
public String getUnit()
{
return "km/h";
}
@Override
public Class<Speed> getValueType()
{
return Speed.class;
}
};
/** Harmonic mean speed measurement. */
public static final LoopDetectorMeasurement<Double, Speed> HARMONIC_MEAN_SPEED =
new LoopDetectorMeasurement<Double, Speed>()
{
@Override
public Double identity()
{
return 0.0;
}
@Override
public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
return cumulative + (1.0 / gtu.getSpeed().si);
}
@Override
public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
return cumulative;
}
@Override
public boolean isPeriodic()
{
return true;
}
@Override
public Speed aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
{
return new Speed(3.6 * gtuCount / cumulative, SpeedUnit.KM_PER_HOUR);
}
@Override
public String getName()
{
return "vHarm";
}
@Override
public String toString()
{
return getName();
}
@Override
public String getDescription()
{
return "harmonic mean speed";
}
@Override
public String getUnit()
{
return "km/h";
}
@Override
public Class<Speed> getValueType()
{
return Speed.class;
}
};
/** Occupancy measurement. */
public static final LoopDetectorMeasurement<Double, Double> OCCUPANCY = new LoopDetectorMeasurement<Double, Double>()
{
/** Time the last GTU triggered the upstream detector. */
private double lastEntry = Double.NaN;
/** Id of last GTU that triggered the upstream detector. */
private String lastId;
@Override
public Double identity()
{
return 0.0;
}
@Override
public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
this.lastEntry = gtu.getSimulator().getSimulatorTime().si;
this.lastId = gtu.getId();
return cumulative;
}
@Override
public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
{
if (!gtu.getId().equals(this.lastId))
{
// vehicle entered the lane along the length of the detector; we can skip as occupancy detector are not long
return cumulative;
}
this.lastId = null;
return cumulative + (gtu.getSimulator().getSimulatorTime().si - this.lastEntry);
}
@Override
public boolean isPeriodic()
{
return true;
}
@Override
public Double aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
{
return cumulative / aggregation.si;
}
@Override
public String getName()
{
return "occupancy";
}
@Override
public String toString()
{
return getName();
}
@Override
public String getDescription()
{
return "occupancy as fraction of time";
}
@Override
public Class<Double> getValueType()
{
return Double.class;
}
};
/** Passages measurement. */
public static final LoopDetectorMeasurement<List<Duration>, List<Duration>> PASSAGES =
new LoopDetectorMeasurement<List<Duration>, List<Duration>>()
{
@Override
public List<Duration> identity()
{
return new ArrayList<>();
}
@Override
public List<Duration> accumulateEntry(final List<Duration> cumulative, final LaneBasedGtu gtu,
final LoopDetector loopDetector)
{
cumulative.add(gtu.getSimulator().getSimulatorTime());
return cumulative;
}
@Override
public List<Duration> accumulateExit(final List<Duration> cumulative, final LaneBasedGtu gtu,
final LoopDetector loopDetector)
{
return cumulative;
}
@Override
public boolean isPeriodic()
{
return false;
}
@Override
public List<Duration> aggregate(final List<Duration> cumulative, final int gtuCount, final Duration aggregation)
{
return cumulative;
}
@Override
public String getName()
{
return "passage times";
}
@Override
public String toString()
{
return getName();
}
@Override
public String getDescription()
{
return "list of vehicle passage time";
}
@Override
public String getUnit()
{
return "s";
}
@Override
public Class<Duration> getValueType()
{
return Duration.class;
}
};
/** Aggregation time. */
private final Duration aggregation;
/** Aggregation time of current period, may differ due to offset at start. */
private Duration currentAggregation;
/** First aggregation. */
private final Time firstAggregation;
/** Flow per aggregation period. */
private final List<Frequency> flow = new ArrayList<>();
/** Measurements per aggregation period. */
private final Map<LoopDetectorMeasurement<?, ?>, List<?>> periodicDataMap = new LinkedHashMap<>();
/** Detector length. */
private final Length length;
/** Period number. */
private int period = 1;
/** Count in current period. */
private int gtuCountCurrentPeriod = 0;
/** Count overall. */
private int overallGtuCount = 0;
/** Current cumulative measurements. */
private final Map<LoopDetectorMeasurement<?, ?>, Object> currentCumulativeDataMap = new LinkedHashMap<>();
/**
* Constructor for regular Dutch dual-loop detectors measuring flow and mean speed aggregated over 60s.
* @param id String; detector id
* @param lane Lane; lane
* @param longitudinalPosition Length; position
* @param simulator OtsSimulatorInterface; simulator
* @param detectorType DetectorType; detector type.
* @throws NetworkException on network exception
*/
public LoopDetector(final String id, final Lane lane, final Length longitudinalPosition, final DetectorType detectorType,
final OtsSimulatorInterface simulator) throws NetworkException
{
// Note: length not important for flow and mean speed
this(id, lane, longitudinalPosition, Length.ZERO, detectorType, simulator, Time.instantiateSI(60.0),
Duration.instantiateSI(60.0), MEAN_SPEED);
}
/**
* Constructor.
* @param id String; detector id
* @param lane Lane; lane
* @param longitudinalPosition Length; position
* @param length Length; length
* @param simulator OtsSimulatorInterface; simulator
* @param firstAggregation Time; time of first aggregation
* @param aggregation Duration; aggregation period
* @param measurements DetectorMeasurement<?, ?>...; measurements to obtain
* @param detectorType DetectorType; detector type.
* @throws NetworkException on network exception
*/
public LoopDetector(final String id, final Lane lane, final Length longitudinalPosition, final Length length,
final DetectorType detectorType, final OtsSimulatorInterface simulator, final Time firstAggregation,
final Duration aggregation, final LoopDetectorMeasurement<?, ?>... measurements) throws NetworkException
{
super(id, lane, longitudinalPosition, RelativePosition.FRONT, simulator, detectorType);
Throw.when(aggregation.si <= 0.0, IllegalArgumentException.class, "Aggregation time should be positive.");
this.length = length;
this.currentAggregation = Duration.instantiateSI(firstAggregation.si);
this.aggregation = aggregation;
this.firstAggregation = firstAggregation;
Try.execute(() -> simulator.scheduleEventAbsTime(firstAggregation, this, "aggregate", null), "");
for (LoopDetectorMeasurement<?, ?> measurement : measurements)
{
this.currentCumulativeDataMap.put(measurement, measurement.identity());
if (measurement.isPeriodic())
{
this.periodicDataMap.put(measurement, new ArrayList<>());
}
}
// rear detector
/** Abstract detector. */
class RearDetector extends LaneDetector
{
/** */
private static final long serialVersionUID = 20180315L;
/**
* Constructor.
* @param idRear String; id
* @param laneRear Lane; lane
* @param longitudinalPositionRear Length; position
* @param simulatorRear OtsSimulatorInterface; simulator
* @param detectorType DetectorType; detector type.
* @throws NetworkException on network exception
*/
@SuppressWarnings("synthetic-access")
RearDetector(final String idRear, final Lane laneRear, final Length longitudinalPositionRear,
final OtsSimulatorInterface simulatorRear, final DetectorType detectorType) throws NetworkException
{
super(idRear, laneRear, longitudinalPositionRear, RelativePosition.REAR, simulatorRear, detectorType);
}
/** {@inheritDoc} */
@SuppressWarnings("synthetic-access")
@Override
protected void triggerResponse(final LaneBasedGtu gtu)
{
for (LoopDetectorMeasurement<?, ?> measurement : LoopDetector.this.currentCumulativeDataMap.keySet())
{
accumulate(measurement, gtu, false);
}
}
}
Length position = longitudinalPosition.plus(length);
Throw.when(position.gt(lane.getLength()), IllegalStateException.class,
"A Detector can not be placed at a lane boundary");
new RearDetector(id + "_rear", lane, position, simulator, detectorType);
}
/** {@inheritDoc} */
@Override
public Length getLength()
{
return this.length;
}
/** {@inheritDoc} */
@Override
protected void triggerResponse(final LaneBasedGtu gtu)
{
this.gtuCountCurrentPeriod++;
this.overallGtuCount++;
for (LoopDetectorMeasurement<?, ?> measurement : this.currentCumulativeDataMap.keySet())
{
accumulate(measurement, gtu, true);
}
this.fireTimedEvent(LOOP_DETECTOR_TRIGGERED, new Object[] {gtu.getId()}, getSimulator().getSimulatorTime());
}
/**
* Accumulates a measurement.
* @param measurement DetectorMeasurement<C, ?>; measurement to accumulate
* @param gtu LaneBasedGtu; gtu
* @param front boolean; triggered by front entering (or rear leaving when false)
* @param <C> accumulated type
*/
@SuppressWarnings("unchecked")
<C> void accumulate(final LoopDetectorMeasurement<C, ?> measurement, final LaneBasedGtu gtu, final boolean front)
{
if (front)
{
this.currentCumulativeDataMap.put(measurement,
measurement.accumulateEntry((C) this.currentCumulativeDataMap.get(measurement), gtu, this));
}
else
{
this.currentCumulativeDataMap.put(measurement,
measurement.accumulateExit((C) this.currentCumulativeDataMap.get(measurement), gtu, this));
}
}
/**
* Aggregation.
*/
private void aggregate()
{
Frequency frequency = Frequency.instantiateSI(this.gtuCountCurrentPeriod / this.currentAggregation.si);
this.flow.add(frequency);
for (LoopDetectorMeasurement<?, ?> measurement : this.periodicDataMap.keySet())
{
aggregate(measurement, this.gtuCountCurrentPeriod, this.currentAggregation);
this.currentCumulativeDataMap.put(measurement, measurement.identity());
}
this.gtuCountCurrentPeriod = 0;
this.currentAggregation = this.aggregation; // after first possibly irregular period, all periods regular
if (!getListenerReferences(LOOP_DETECTOR_AGGREGATE).isEmpty())
{
Object[] data = new Object[this.periodicDataMap.size() + 1];
data[0] = frequency;
int i = 1;
for (LoopDetectorMeasurement<?, ?> measurement : this.periodicDataMap.keySet())
{
List<?> list = this.periodicDataMap.get(measurement);
data[i] = list.get(list.size() - 1);
i++;
}
this.fireTimedEvent(LOOP_DETECTOR_AGGREGATE, data, getSimulator().getSimulatorTime());
}
Time time = Time.instantiateSI(this.firstAggregation.si + this.aggregation.si * this.period++);
Try.execute(() -> getSimulator().scheduleEventAbsTime(time, this, "aggregate", null), "");
}
/**
* Returns whether the detector has aggregated data available.
* @return boolean; whether the detector has aggregated data available
*/
public boolean hasLastValue()
{
return !this.flow.isEmpty();
}
/**
* Returns the last flow.
* @return last flow
*/
public Frequency getLastFlow()
{
return this.flow.get(this.flow.size() - 1);
}
/**
* Returns the last value of the detector measurement.
* @param detectorMeasurement DetectorMeasurement<?,A>; detector measurement
* @return last value of the detector measurement
* @param <A> aggregate value type of the detector measurement
*/
public <A> A getLastValue(final LoopDetectorMeasurement<?, A> detectorMeasurement)
{
@SuppressWarnings("unchecked")
List<A> list = (List<A>) this.periodicDataMap.get(detectorMeasurement);
return list.get(list.size() - 1);
}
/**
* Aggregates a periodic measurement.
* @param measurement DetectorMeasurement<C, A>; measurement to aggregate
* @param gtuCount int; number of GTUs
* @param agg Duration; aggregation period
* @param <C> accumulated type
* @param <A> aggregated type
*/
@SuppressWarnings("unchecked")
private <C, A> void aggregate(final LoopDetectorMeasurement<C, A> measurement, final int gtuCount, final Duration agg)
{
((List<A>) this.periodicDataMap.get(measurement)).add(getAggregateValue(measurement, gtuCount, agg));
}
/**
* Returns the aggregated value of the measurement.
* @param measurement DetectorMeasurement<C, A>; measurement to aggregate
* @param gtuCount int; number of GTUs
* @param agg Duration; aggregation period
* @return A; aggregated value of the measurement
* @param <C> accumulated type
* @param <A> aggregated type
*/
@SuppressWarnings("unchecked")
private <C, A> A getAggregateValue(final LoopDetectorMeasurement<C, A> measurement, final int gtuCount, final Duration agg)
{
return measurement.aggregate((C) this.currentCumulativeDataMap.get(measurement), gtuCount, agg);
}
/**
* Returns a map of non-periodic measurements, mapping measurement type and the data.
* @return Map<DetectorMeasurement, Object>; map of non-periodic measurements
*/
private Map<LoopDetectorMeasurement<?, ?>, Object> getNonPeriodicMeasurements()
{
Map<LoopDetectorMeasurement<?, ?>, Object> map = new LinkedHashMap<>();
for (LoopDetectorMeasurement<?, ?> measurement : this.currentCumulativeDataMap.keySet())
{
if (!measurement.isPeriodic())
{
map.put(measurement, getAggregateValue(measurement, this.overallGtuCount,
this.getSimulator().getSimulatorAbsTime().minus(Time.ZERO)));
}
}
return map;
}
/**
* Returns a Table with loop detector positions.
* @param network RoadNetwork; network from which all detectors are found.
* @return Table; with loop detector positions.
*/
public static Table asTablePositions(final RoadNetwork network)
{
Set<LoopDetector> detectors = getLoopDetectors(network);
Collection<Column<?>> columns = new LinkedHashSet<>();
columns.add(new Column<>("id", "detector id", String.class, null));
columns.add(new Column<>("laneId", "lane id", String.class, null));
columns.add(new Column<>("linkId", "link id", String.class, null));
columns.add(new Column<>("position", "detector position on the lane", Length.class, "m"));
return new Table("detectors", "list of all loop-detectors", columns)
{
/** {@inheritDoc} */
@Override
public Iterator<Row> iterator()
{
Iterator<LoopDetector> iterator = detectors.iterator();
return new Iterator<>()
{
/** {@inheritDoc} */
@Override
public boolean hasNext()
{
return iterator.hasNext();
}
/** {@inheritDoc} */
@Override
public Row next()
{
LoopDetector detector = iterator.next();
return new Row(table(), new Object[] {detector.getId(), detector.getLane().getId(),
detector.getLane().getLink().getId(), detector.getLongitudinalPosition()});
}
};
}
/**
* Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
* @return Table; this table instance for inner classes.
*/
private Table table()
{
return this;
}
/** {@inheritDoc} */
@Override
public boolean isEmpty()
{
return detectors.isEmpty();
}
};
}
/**
* Returns a Table with all periodic data, such as flow and speed per minute.
* @param network RoadNetwork; network from which all detectors are found.
* @return Table; with all periodic data, such as flow and speed per minute.
*/
public static Table asTablePeriodicData(final RoadNetwork network)
{
Set<LoopDetector> detectors = getLoopDetectors(network);
Set<LoopDetectorMeasurement<?, ?>> measurements = getMeasurements(detectors, true);
Collection<Column<?>> columns = new LinkedHashSet<>();
columns.add(new Column<>("id", "detector id", String.class, null));
columns.add(new Column<>("t", "time (start of aggregation period)", Duration.class, "s"));
columns.add(new Column<>("q", "flow", Frequency.class, "/h"));
for (LoopDetectorMeasurement<?, ?> measurement : measurements)
{
columns.add(new Column<>(measurement.getName(), measurement.getDescription(), measurement.getValueType(),
measurement.getUnit()));
}
return new Table("periodic", "periodic measurements", columns)
{
/** {@inheritDoc} */
@Override
public Iterator<Row> iterator()
{
Iterator<LoopDetector> iterator = detectors.iterator();
return new Iterator<>()
{
/** Index iterator. */
private Iterator<Integer> indexIterator = Collections.emptyIterator();
/** Current loop detector. */
private LoopDetector loopDetector;
/** Map of measurement data per measurement, updated for each detector. */
private Map<LoopDetectorMeasurement<?, ?>, List<?>> map;
/** {@inheritDoc} */
@Override
public boolean hasNext()
{
while (!this.indexIterator.hasNext())
{
if (!iterator.hasNext())
{
return false;
}
this.loopDetector = iterator.next();
this.loopDetector.aggregate();
this.indexIterator = IntStream.range(0, this.loopDetector.flow.size()).iterator();
this.map = this.loopDetector.periodicDataMap;
}
return true;
}
/** {@inheritDoc} */
@Override
public Row next()
{
Throw.when(!hasNext(), NoSuchElementException.class, "Periodic data unavailable.");
Object[] data = new Object[columns.size()];
int index = this.indexIterator.next();
double t = this.loopDetector.firstAggregation.si + (index - 1) * this.loopDetector.aggregation.si;
data[0] = this.loopDetector.getId();
data[1] = Duration.instantiateSI(t < 0.0 ? 0.0 : t);
data[2] = this.loopDetector.flow.get(index);
int dataIndex = 3;
for (LoopDetectorMeasurement<?, ?> measurement : measurements)
{
if (this.map.containsKey(measurement))
{
data[dataIndex++] = this.map.get(measurement).get(index);
}
else
{
// this data is not available for this detector
data[dataIndex++] = null;
}
}
return new Row(table(), data);
}
};
}
/**
* Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
* @return Table; this table instance for inner classes.
*/
private Table table()
{
return this;
}
/** {@inheritDoc} */
@Override
public boolean isEmpty()
{
return detectors.isEmpty() || !detectors.iterator().next().hasLastValue();
}
};
}
/**
* Returns a Table with all non-periodic data, such as vehicle passage times or platoon counts.
* @param network RoadNetwork; network from which all detectors are found.
* @return Table; with all non-periodic data, such as vehicle passage times or platoon counts.
*/
public static Table asTableNonPeriodicData(final RoadNetwork network)
{
Set<LoopDetector> detectors = getLoopDetectors(network);
Set<LoopDetectorMeasurement<?, ?>> measurements = getMeasurements(detectors, false);
Collection<Column<?>> columns = new LinkedHashSet<>();
columns.add(new Column<>("id", "detector id", String.class, null));
columns.add(new Column<>("measurement", "measurement type", String.class, null));
columns.add(new Column<>("data", "data in any form", String.class, null));
return new Table("non-periodic", "non-periodic measurements", columns)
{
/** {@inheritDoc} */
@Override
public Iterator<Row> iterator()
{
Iterator<LoopDetector> iterator = detectors.iterator();
return new Iterator<>()
{
/** Index iterator. */
private Iterator<LoopDetectorMeasurement<?, ?>> measurementIterator = Collections.emptyIterator();
/** Current loop detector. */
private LoopDetector loopDetector;
/** Map of measurement data per measurement, updated for each detector. */
private Map<LoopDetectorMeasurement<?, ?>, Object> map;
/** Current measurement. */
private LoopDetectorMeasurement<?, ?> measurement;
/** {@inheritDoc} */
@Override
public boolean hasNext()
{
if (this.measurement != null)
{
return true; // catch consecutive 'hasNext' calls before next 'next' call
}
while (!this.measurementIterator.hasNext())
{
if (!iterator.hasNext())
{
return false;
}
this.loopDetector = iterator.next();
this.measurementIterator = measurements.iterator();
this.map = this.loopDetector.getNonPeriodicMeasurements();
}
// skip if data is not available for this detector
this.measurement = this.measurementIterator.next();
if (!this.map.containsKey(this.measurement))
{
this.measurement = null;
return hasNext();
}
return true;
}
/** {@inheritDoc} */
@Override
public Row next()
{
Throw.when(!hasNext(), NoSuchElementException.class, "Non-periodic data unavailable.");
Object[] data = new Object[columns.size()];
data[0] = this.loopDetector.getId();
data[1] = this.measurement.getName();
data[2] = this.map.get(this.measurement).toString();
this.measurement = null;
return new Row(table(), data);
}
};
}
/**
* Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
* @return Table; this table instance for inner classes.
*/
private Table table()
{
return this;
}
/** {@inheritDoc} */
@Override
public boolean isEmpty()
{
return detectors.isEmpty() || measurements.isEmpty();
}
};
}
/**
* Gathers all loop detectors from the network and puts them in a set sorted by loop detector id.
* @param network RoadNetwork; network.
* @return Set<LoopDetector>; set of loop detector sorted by loop detector id.
*/
private static Set<LoopDetector> getLoopDetectors(final RoadNetwork network)
{
Set<LoopDetector> detectors = new TreeSet<>(new Comparator<LoopDetector>()
{
@Override
public int compare(final LoopDetector o1, final LoopDetector o2)
{
return o1.getId().compareTo(o2.getId());
}
});
detectors.addAll(network.getObjectMap(LoopDetector.class).values().toCollection());
return detectors;
}
/**
* Returns all measurement type that are found accross a set of loop detectors.
* @param detectors Set<LoopDetector>; set of loop detectors.
* @param periodic boolean; gather the periodic measurements {@code true}, or the non-periodic measurements {@code false}.
* @return Set<LoopDetectorMeasurement<?, ?>>; set of periodic or non-periodic measurements from the detectors.
*/
private static Set<LoopDetectorMeasurement<?, ?>> getMeasurements(final Set<LoopDetector> detectors, final boolean periodic)
{
return detectors.stream().flatMap((det) -> det.currentCumulativeDataMap.keySet().stream())
.filter((measurement) -> measurement.isPeriodic() == periodic)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Interface for what detectors measure.
* <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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
* @param <C> accumulated type
* @param <A> aggregated type
*/
public interface LoopDetectorMeasurement<C, A>
{
/**
* Returns the initial value before accumulation.
* @return C; initial value before accumulation
*/
C identity();
/**
* Returns an accumulated value for when the front reaches the detector. GTU's may trigger an exit without having
* triggered an entry due to a lane change. Reversely, GTU's may not trigger an exit while they did trigger an entry.
* @param cumulative C; accumulated value
* @param gtu LaneBasedGtu; gtu
* @param loopDetector Detector; loop detector
* @return C; accumulated value
*/
C accumulateEntry(C cumulative, LaneBasedGtu gtu, LoopDetector loopDetector);
/**
* Returns an accumulated value for when the rear leaves the detector. GTU's may trigger an exit without having
* triggered an entry due to a lane change. Reversely, GTU's may not trigger an exit while they did trigger an entry.
* @param cumulative C; accumulated value
* @param gtu LaneBasedGtu; gtu
* @param loopDetector Detector; loop detector
* @return C; accumulated value
*/
C accumulateExit(C cumulative, LaneBasedGtu gtu, LoopDetector loopDetector);
/**
* Returns whether the measurement aggregates every aggregation period (or only over the entire simulation).
* @return boolean; whether the measurement aggregates every aggregation period (or only over the entire simulation)
*/
boolean isPeriodic();
/**
* Returns an aggregated value.
* @param cumulative C; accumulated value
* @param gtuCount int; GTU gtuCount
* @param aggregation Duration; aggregation period
* @return A; aggregated value
*/
A aggregate(C cumulative, int gtuCount, Duration aggregation);
/**
* Returns the value name.
* @return String; value name
*/
String getName();
/**
* Measurement description.
* @return String; measurement description.
*/
String getDescription();
/**
* Returns the unit string, default is {@code null}.
* @return String; unit string.
*/
default String getUnit()
{
return null;
}
/**
* Returns the data type.
* @return Class<?>; data type.
*/
Class<?> getValueType();
}
/**
* Measurement of platoon sizes based on time between previous GTU exit and GTU entry.
* <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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public static class PlatoonSizes implements LoopDetectorMeasurement<PlatoonMeasurement, List<Integer>>
{
/** Maximum time between two vehicles that are considered to be in the same platoon. */
private final Duration threshold;
/**
* Constructor.
* @param threshold Duration; maximum time between two vehicles that are considered to be in the same platoon
*/
public PlatoonSizes(final Duration threshold)
{
this.threshold = threshold;
}
/** {@inheritDoc} */
@Override
public PlatoonMeasurement identity()
{
return new PlatoonMeasurement();
}
/** {@inheritDoc} */
@SuppressWarnings("synthetic-access")
@Override
public PlatoonMeasurement accumulateEntry(final PlatoonMeasurement cumulative, final LaneBasedGtu gtu,
final LoopDetector loopDetector)
{
Time now = gtu.getSimulator().getSimulatorAbsTime();
if (now.si - cumulative.lastExitTime.si < this.threshold.si)
{
cumulative.gtuCount++;
}
else
{
if (cumulative.gtuCount > 0) // 0 means this is the first vehicle of the first platoon
{
cumulative.platoons.add(cumulative.gtuCount);
}
cumulative.gtuCount = 1;
}
cumulative.enteredGTUs.add(gtu);
cumulative.lastExitTime = now; // should we change lane before triggering the exit
return cumulative;
}
/** {@inheritDoc} */
@SuppressWarnings("synthetic-access")
@Override
public PlatoonMeasurement accumulateExit(final PlatoonMeasurement cumulative, final LaneBasedGtu gtu,
final LoopDetector loopDetector)
{
int index = cumulative.enteredGTUs.indexOf(gtu);
if (index >= 0)
{
cumulative.lastExitTime = gtu.getSimulator().getSimulatorAbsTime();
// gtu is likely the oldest gtu in the list at index 0, but sometimes an older gtu may have left the detector by
// changing lane, by clearing up to this gtu, older gtu's are automatically removed
cumulative.enteredGTUs.subList(0, index).clear();
}
return cumulative;
}
/** {@inheritDoc} */
@Override
public boolean isPeriodic()
{
return false;
}
/** {@inheritDoc} */
@SuppressWarnings("synthetic-access")
@Override
public List<Integer> aggregate(final PlatoonMeasurement cumulative, final int count, final Duration aggregation)
{
if (cumulative.gtuCount > 0)
{
cumulative.platoons.add(cumulative.gtuCount);
cumulative.gtuCount = 0; // prevent that the last platoon is added again if the same output is saved again
}
return cumulative.platoons;
}
/** {@inheritDoc} */
@Override
public String getName()
{
return "platoon sizes";
}
/** {@inheritDoc} */
@Override
public String toString()
{
return getName();
}
/** {@inheritDoc} */
@Override
public String getDescription()
{
return "list of platoon sizes (threshold: " + this.threshold + ")";
}
/** {@inheritDoc} */
@Override
public Class<Integer> getValueType()
{
return Integer.class;
}
}
/**
* Cumulative information for platoon size measurement.
* <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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
static class PlatoonMeasurement
{
/** GTU's counted so far in the current platoon. */
private int gtuCount = 0;
/** Time the last GTU exited the detector. */
private Time lastExitTime = Time.instantiateSI(Double.NEGATIVE_INFINITY);
/** Stored sizes of earlier platoons. */
private List<Integer> platoons = new ArrayList<>();
/** GTU's currently on the detector, some may have left by a lane change. */
private List<LaneBasedGtu> enteredGTUs = new ArrayList<>();
}
}