LoopDetector.java

  1. package org.opentrafficsim.road.network.lane.object.detector;

  2. import java.util.ArrayList;
  3. import java.util.Collection;
  4. import java.util.Collections;
  5. import java.util.Comparator;
  6. import java.util.Iterator;
  7. import java.util.LinkedHashMap;
  8. import java.util.LinkedHashSet;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.NoSuchElementException;
  12. import java.util.Set;
  13. import java.util.TreeSet;
  14. import java.util.stream.Collectors;
  15. import java.util.stream.IntStream;

  16. import org.djunits.unit.SpeedUnit;
  17. import org.djunits.value.vdouble.scalar.Duration;
  18. import org.djunits.value.vdouble.scalar.Frequency;
  19. import org.djunits.value.vdouble.scalar.Length;
  20. import org.djunits.value.vdouble.scalar.Speed;
  21. import org.djunits.value.vdouble.scalar.Time;
  22. import org.djutils.data.Column;
  23. import org.djutils.data.Row;
  24. import org.djutils.data.Table;
  25. import org.djutils.event.EventType;
  26. import org.djutils.exceptions.Throw;
  27. import org.djutils.exceptions.Try;
  28. import org.djutils.metadata.MetaData;
  29. import org.djutils.metadata.ObjectDescriptor;
  30. import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
  31. import org.opentrafficsim.core.gtu.RelativePosition;
  32. import org.opentrafficsim.core.network.NetworkException;
  33. import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
  34. import org.opentrafficsim.road.network.RoadNetwork;
  35. import org.opentrafficsim.road.network.lane.Lane;

  36. /**
  37.  * Detector, measuring a dynamic set of measurements typical for single- or dual-loop detectors. A subsidiary detector is placed
  38.  * at a position downstream based on the detector length, that triggers when the rear leaves the detector.
  39.  * <p>
  40.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  41.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  42.  * </p>
  43.  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  44.  * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  45.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  46.  */
  47. public class LoopDetector extends LaneDetector
  48. {

  49.     /** */
  50.     private static final long serialVersionUID = 20180312L;

  51.     /** Trigger event. Payload: [Id of LaneBasedGtu]. */
  52.     public static final EventType LOOP_DETECTOR_TRIGGERED =
  53.             new EventType("LOOPDETECTOR.TRIGGER", new MetaData("Dual loop detector triggered", "Dual loop detector triggered",
  54.                     new ObjectDescriptor[] {new ObjectDescriptor("Id of GTU", "Id of GTU", String.class)}));

  55.     /** Aggregation event. Payload: [Frequency, measurement...]/ */
  56.     public static final EventType LOOP_DETECTOR_AGGREGATE = new EventType("LOOPDETECTOR.AGGREGATE", MetaData.NO_META_DATA);

  57.     /** Mean speed measurement. */
  58.     public static final LoopDetectorMeasurement<Double, Speed> MEAN_SPEED = new LoopDetectorMeasurement<Double, Speed>()
  59.     {
  60.         @Override
  61.         public Double identity()
  62.         {
  63.             return 0.0;
  64.         }

  65.         @Override
  66.         public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  67.         {
  68.             return cumulative + gtu.getSpeed().si;
  69.         }

  70.         @Override
  71.         public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  72.         {
  73.             return cumulative;
  74.         }

  75.         @Override
  76.         public boolean isPeriodic()
  77.         {
  78.             return true;
  79.         }

  80.         @Override
  81.         public Speed aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
  82.         {
  83.             return new Speed(3.6 * cumulative / gtuCount, SpeedUnit.KM_PER_HOUR);
  84.         }

  85.         @Override
  86.         public String getName()
  87.         {
  88.             return "v";
  89.         }

  90.         @Override
  91.         public String toString()
  92.         {
  93.             return getName();
  94.         }

  95.         @Override
  96.         public String getDescription()
  97.         {
  98.             return "mean speed";
  99.         }

  100.         @Override
  101.         public String getUnit()
  102.         {
  103.             return "km/h";
  104.         }

  105.         @Override
  106.         public Class<Speed> getValueType()
  107.         {
  108.             return Speed.class;
  109.         }
  110.     };

  111.     /** Harmonic mean speed measurement. */
  112.     public static final LoopDetectorMeasurement<Double, Speed> HARMONIC_MEAN_SPEED =
  113.             new LoopDetectorMeasurement<Double, Speed>()
  114.             {
  115.                 @Override
  116.                 public Double identity()
  117.                 {
  118.                     return 0.0;
  119.                 }

  120.                 @Override
  121.                 public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  122.                 {
  123.                     return cumulative + (1.0 / gtu.getSpeed().si);
  124.                 }

  125.                 @Override
  126.                 public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  127.                 {
  128.                     return cumulative;
  129.                 }

  130.                 @Override
  131.                 public boolean isPeriodic()
  132.                 {
  133.                     return true;
  134.                 }

  135.                 @Override
  136.                 public Speed aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
  137.                 {
  138.                     return new Speed(3.6 * gtuCount / cumulative, SpeedUnit.KM_PER_HOUR);
  139.                 }

  140.                 @Override
  141.                 public String getName()
  142.                 {
  143.                     return "vHarm";
  144.                 }

  145.                 @Override
  146.                 public String toString()
  147.                 {
  148.                     return getName();
  149.                 }

  150.                 @Override
  151.                 public String getDescription()
  152.                 {
  153.                     return "harmonic mean speed";
  154.                 }

  155.                 @Override
  156.                 public String getUnit()
  157.                 {
  158.                     return "km/h";
  159.                 }

  160.                 @Override
  161.                 public Class<Speed> getValueType()
  162.                 {
  163.                     return Speed.class;
  164.                 }
  165.             };

  166.     /** Occupancy measurement. */
  167.     public static final LoopDetectorMeasurement<Double, Double> OCCUPANCY = new LoopDetectorMeasurement<Double, Double>()
  168.     {
  169.         /** Time the last GTU triggered the upstream detector. */
  170.         private double lastEntry = Double.NaN;

  171.         /** Id of last GTU that triggered the upstream detector. */
  172.         private String lastId;

  173.         @Override
  174.         public Double identity()
  175.         {
  176.             return 0.0;
  177.         }

  178.         @Override
  179.         public Double accumulateEntry(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  180.         {
  181.             this.lastEntry = gtu.getSimulator().getSimulatorTime().si;
  182.             this.lastId = gtu.getId();
  183.             return cumulative;
  184.         }

  185.         @Override
  186.         public Double accumulateExit(final Double cumulative, final LaneBasedGtu gtu, final LoopDetector loopDetector)
  187.         {
  188.             if (!gtu.getId().equals(this.lastId))
  189.             {
  190.                 // vehicle entered the lane along the length of the detector; we can skip as occupancy detector are not long
  191.                 return cumulative;
  192.             }
  193.             this.lastId = null;
  194.             return cumulative + (gtu.getSimulator().getSimulatorTime().si - this.lastEntry);
  195.         }

  196.         @Override
  197.         public boolean isPeriodic()
  198.         {
  199.             return true;
  200.         }

  201.         @Override
  202.         public Double aggregate(final Double cumulative, final int gtuCount, final Duration aggregation)
  203.         {
  204.             return cumulative / aggregation.si;
  205.         }

  206.         @Override
  207.         public String getName()
  208.         {
  209.             return "occupancy";
  210.         }

  211.         @Override
  212.         public String toString()
  213.         {
  214.             return getName();
  215.         }

  216.         @Override
  217.         public String getDescription()
  218.         {
  219.             return "occupancy as fraction of time";
  220.         }

  221.         @Override
  222.         public Class<Double> getValueType()
  223.         {
  224.             return Double.class;
  225.         }
  226.     };

  227.     /** Passages measurement. */
  228.     public static final LoopDetectorMeasurement<List<Duration>, List<Duration>> PASSAGES =
  229.             new LoopDetectorMeasurement<List<Duration>, List<Duration>>()
  230.             {
  231.                 @Override
  232.                 public List<Duration> identity()
  233.                 {
  234.                     return new ArrayList<>();
  235.                 }

  236.                 @Override
  237.                 public List<Duration> accumulateEntry(final List<Duration> cumulative, final LaneBasedGtu gtu,
  238.                         final LoopDetector loopDetector)
  239.                 {
  240.                     cumulative.add(gtu.getSimulator().getSimulatorTime());
  241.                     return cumulative;
  242.                 }

  243.                 @Override
  244.                 public List<Duration> accumulateExit(final List<Duration> cumulative, final LaneBasedGtu gtu,
  245.                         final LoopDetector loopDetector)
  246.                 {
  247.                     return cumulative;
  248.                 }

  249.                 @Override
  250.                 public boolean isPeriodic()
  251.                 {
  252.                     return false;
  253.                 }

  254.                 @Override
  255.                 public List<Duration> aggregate(final List<Duration> cumulative, final int gtuCount, final Duration aggregation)
  256.                 {
  257.                     return cumulative;
  258.                 }

  259.                 @Override
  260.                 public String getName()
  261.                 {
  262.                     return "passage times";
  263.                 }

  264.                 @Override
  265.                 public String toString()
  266.                 {
  267.                     return getName();
  268.                 }

  269.                 @Override
  270.                 public String getDescription()
  271.                 {
  272.                     return "list of vehicle passage time";
  273.                 }

  274.                 @Override
  275.                 public String getUnit()
  276.                 {
  277.                     return "s";
  278.                 }

  279.                 @Override
  280.                 public Class<Duration> getValueType()
  281.                 {
  282.                     return Duration.class;
  283.                 }
  284.             };

  285.     /** Aggregation time. */
  286.     private final Duration aggregation;

  287.     /** Aggregation time of current period, may differ due to offset at start. */
  288.     private Duration currentAggregation;

  289.     /** First aggregation. */
  290.     private final Time firstAggregation;

  291.     /** Flow per aggregation period. */
  292.     private final List<Frequency> flow = new ArrayList<>();

  293.     /** Measurements per aggregation period. */
  294.     private final Map<LoopDetectorMeasurement<?, ?>, List<?>> periodicDataMap = new LinkedHashMap<>();

  295.     /** Detector length. */
  296.     private final Length length;

  297.     /** Period number. */
  298.     private int period = 1;

  299.     /** Count in current period. */
  300.     private int gtuCountCurrentPeriod = 0;

  301.     /** Count overall. */
  302.     private int overallGtuCount = 0;

  303.     /** Current cumulative measurements. */
  304.     private final Map<LoopDetectorMeasurement<?, ?>, Object> currentCumulativeDataMap = new LinkedHashMap<>();

  305.     /**
  306.      * Constructor for regular Dutch dual-loop detectors measuring flow and mean speed aggregated over 60s.
  307.      * @param id String; detector id
  308.      * @param lane Lane; lane
  309.      * @param longitudinalPosition Length; position
  310.      * @param simulator OtsSimulatorInterface; simulator
  311.      * @param detectorType DetectorType; detector type.
  312.      * @throws NetworkException on network exception
  313.      */
  314.     public LoopDetector(final String id, final Lane lane, final Length longitudinalPosition, final DetectorType detectorType,
  315.             final OtsSimulatorInterface simulator) throws NetworkException
  316.     {
  317.         // Note: length not important for flow and mean speed
  318.         this(id, lane, longitudinalPosition, Length.ZERO, detectorType, simulator, Time.instantiateSI(60.0),
  319.                 Duration.instantiateSI(60.0), MEAN_SPEED);
  320.     }

  321.     /**
  322.      * Constructor.
  323.      * @param id String; detector id
  324.      * @param lane Lane; lane
  325.      * @param longitudinalPosition Length; position
  326.      * @param length Length; length
  327.      * @param simulator OtsSimulatorInterface; simulator
  328.      * @param firstAggregation Time; time of first aggregation
  329.      * @param aggregation Duration; aggregation period
  330.      * @param measurements DetectorMeasurement&lt;?, ?&gt;...; measurements to obtain
  331.      * @param detectorType DetectorType; detector type.
  332.      * @throws NetworkException on network exception
  333.      */
  334.     public LoopDetector(final String id, final Lane lane, final Length longitudinalPosition, final Length length,
  335.             final DetectorType detectorType, final OtsSimulatorInterface simulator, final Time firstAggregation,
  336.             final Duration aggregation, final LoopDetectorMeasurement<?, ?>... measurements) throws NetworkException
  337.     {
  338.         super(id, lane, longitudinalPosition, RelativePosition.FRONT, simulator, detectorType);
  339.         Throw.when(aggregation.si <= 0.0, IllegalArgumentException.class, "Aggregation time should be positive.");
  340.         this.length = length;
  341.         this.currentAggregation = Duration.instantiateSI(firstAggregation.si);
  342.         this.aggregation = aggregation;
  343.         this.firstAggregation = firstAggregation;
  344.         Try.execute(() -> simulator.scheduleEventAbsTime(firstAggregation, this, "aggregate", null), "");
  345.         for (LoopDetectorMeasurement<?, ?> measurement : measurements)
  346.         {
  347.             this.currentCumulativeDataMap.put(measurement, measurement.identity());
  348.             if (measurement.isPeriodic())
  349.             {
  350.                 this.periodicDataMap.put(measurement, new ArrayList<>());
  351.             }
  352.         }

  353.         // rear detector
  354.         /** Abstract detector. */
  355.         class RearDetector extends LaneDetector
  356.         {
  357.             /** */
  358.             private static final long serialVersionUID = 20180315L;

  359.             /**
  360.              * Constructor.
  361.              * @param idRear String; id
  362.              * @param laneRear Lane; lane
  363.              * @param longitudinalPositionRear Length; position
  364.              * @param simulatorRear OtsSimulatorInterface; simulator
  365.              * @param detectorType DetectorType; detector type.
  366.              * @throws NetworkException on network exception
  367.              */
  368.             @SuppressWarnings("synthetic-access")
  369.             RearDetector(final String idRear, final Lane laneRear, final Length longitudinalPositionRear,
  370.                     final OtsSimulatorInterface simulatorRear, final DetectorType detectorType) throws NetworkException
  371.             {
  372.                 super(idRear, laneRear, longitudinalPositionRear, RelativePosition.REAR, simulatorRear, detectorType);
  373.             }

  374.             /** {@inheritDoc} */
  375.             @SuppressWarnings("synthetic-access")
  376.             @Override
  377.             protected void triggerResponse(final LaneBasedGtu gtu)
  378.             {
  379.                 for (LoopDetectorMeasurement<?, ?> measurement : LoopDetector.this.currentCumulativeDataMap.keySet())
  380.                 {
  381.                     accumulate(measurement, gtu, false);
  382.                 }
  383.             }
  384.         }
  385.         Length position = longitudinalPosition.plus(length);
  386.         Throw.when(position.gt(lane.getLength()), IllegalStateException.class,
  387.                 "A Detector can not be placed at a lane boundary");
  388.         new RearDetector(id + "_rear", lane, position, simulator, detectorType);
  389.     }

  390.     /**
  391.      * Returns the detector length.
  392.      * @return Length; the detector length
  393.      */
  394.     public Length getLength()
  395.     {
  396.         return this.length;
  397.     }

  398.     /** {@inheritDoc} */
  399.     @Override
  400.     protected void triggerResponse(final LaneBasedGtu gtu)
  401.     {
  402.         this.gtuCountCurrentPeriod++;
  403.         this.overallGtuCount++;
  404.         for (LoopDetectorMeasurement<?, ?> measurement : this.currentCumulativeDataMap.keySet())
  405.         {
  406.             accumulate(measurement, gtu, true);
  407.         }
  408.         this.fireTimedEvent(LOOP_DETECTOR_TRIGGERED, new Object[] {gtu.getId()}, getSimulator().getSimulatorTime());
  409.     }

  410.     /**
  411.      * Accumulates a measurement.
  412.      * @param measurement DetectorMeasurement&lt;C, ?&gt;; measurement to accumulate
  413.      * @param gtu LaneBasedGtu; gtu
  414.      * @param front boolean; triggered by front entering (or rear leaving when false)
  415.      * @param <C> accumulated type
  416.      */
  417.     @SuppressWarnings("unchecked")
  418.     <C> void accumulate(final LoopDetectorMeasurement<C, ?> measurement, final LaneBasedGtu gtu, final boolean front)
  419.     {
  420.         if (front)
  421.         {
  422.             this.currentCumulativeDataMap.put(measurement,
  423.                     measurement.accumulateEntry((C) this.currentCumulativeDataMap.get(measurement), gtu, this));
  424.         }
  425.         else
  426.         {
  427.             this.currentCumulativeDataMap.put(measurement,
  428.                     measurement.accumulateExit((C) this.currentCumulativeDataMap.get(measurement), gtu, this));
  429.         }
  430.     }

  431.     /**
  432.      * Aggregation.
  433.      */
  434.     private void aggregate()
  435.     {
  436.         Frequency frequency = Frequency.instantiateSI(this.gtuCountCurrentPeriod / this.currentAggregation.si);
  437.         this.flow.add(frequency);
  438.         for (LoopDetectorMeasurement<?, ?> measurement : this.periodicDataMap.keySet())
  439.         {
  440.             aggregate(measurement, this.gtuCountCurrentPeriod, this.currentAggregation);
  441.             this.currentCumulativeDataMap.put(measurement, measurement.identity());
  442.         }
  443.         this.gtuCountCurrentPeriod = 0;
  444.         this.currentAggregation = this.aggregation; // after first possibly irregular period, all periods regular
  445.         if (!getListenerReferences(LOOP_DETECTOR_AGGREGATE).isEmpty())
  446.         {
  447.             Object[] data = new Object[this.periodicDataMap.size() + 1];
  448.             data[0] = frequency;
  449.             int i = 1;
  450.             for (LoopDetectorMeasurement<?, ?> measurement : this.periodicDataMap.keySet())
  451.             {
  452.                 List<?> list = this.periodicDataMap.get(measurement);
  453.                 data[i] = list.get(list.size() - 1);
  454.                 i++;
  455.             }
  456.             this.fireTimedEvent(LOOP_DETECTOR_AGGREGATE, data, getSimulator().getSimulatorTime());
  457.         }
  458.         Time time = Time.instantiateSI(this.firstAggregation.si + this.aggregation.si * this.period++);
  459.         Try.execute(() -> getSimulator().scheduleEventAbsTime(time, this, "aggregate", null), "");
  460.     }

  461.     /**
  462.      * Returns whether the detector has aggregated data available.
  463.      * @return boolean; whether the detector has aggregated data available
  464.      */
  465.     public boolean hasLastValue()
  466.     {
  467.         return !this.flow.isEmpty();
  468.     }

  469.     /**
  470.      * Returns the last flow.
  471.      * @return last flow
  472.      */
  473.     public Frequency getLastFlow()
  474.     {
  475.         return this.flow.get(this.flow.size() - 1);
  476.     }

  477.     /**
  478.      * Returns the last value of the detector measurement.
  479.      * @param detectorMeasurement DetectorMeasurement&lt;?,A&gt;; detector measurement
  480.      * @return last value of the detector measurement
  481.      * @param <A> aggregate value type of the detector measurement
  482.      */
  483.     public <A> A getLastValue(final LoopDetectorMeasurement<?, A> detectorMeasurement)
  484.     {
  485.         @SuppressWarnings("unchecked")
  486.         List<A> list = (List<A>) this.periodicDataMap.get(detectorMeasurement);
  487.         return list.get(list.size() - 1);
  488.     }

  489.     /**
  490.      * Aggregates a periodic measurement.
  491.      * @param measurement DetectorMeasurement&lt;C, A&gt;; measurement to aggregate
  492.      * @param gtuCount int; number of GTUs
  493.      * @param agg Duration; aggregation period
  494.      * @param <C> accumulated type
  495.      * @param <A> aggregated type
  496.      */
  497.     @SuppressWarnings("unchecked")
  498.     private <C, A> void aggregate(final LoopDetectorMeasurement<C, A> measurement, final int gtuCount, final Duration agg)
  499.     {
  500.         ((List<A>) this.periodicDataMap.get(measurement)).add(getAggregateValue(measurement, gtuCount, agg));
  501.     }

  502.     /**
  503.      * Returns the aggregated value of the measurement.
  504.      * @param measurement DetectorMeasurement&lt;C, A&gt;; measurement to aggregate
  505.      * @param gtuCount int; number of GTUs
  506.      * @param agg Duration; aggregation period
  507.      * @return A; aggregated value of the measurement
  508.      * @param <C> accumulated type
  509.      * @param <A> aggregated type
  510.      */
  511.     @SuppressWarnings("unchecked")
  512.     private <C, A> A getAggregateValue(final LoopDetectorMeasurement<C, A> measurement, final int gtuCount, final Duration agg)
  513.     {
  514.         return measurement.aggregate((C) this.currentCumulativeDataMap.get(measurement), gtuCount, agg);
  515.     }

  516.     /**
  517.      * Returns a map of non-periodic measurements, mapping measurement type and the data.
  518.      * @return Map&lt;DetectorMeasurement, Object&gt;; map of non-periodic measurements
  519.      */
  520.     private Map<LoopDetectorMeasurement<?, ?>, Object> getNonPeriodicMeasurements()
  521.     {
  522.         Map<LoopDetectorMeasurement<?, ?>, Object> map = new LinkedHashMap<>();
  523.         for (LoopDetectorMeasurement<?, ?> measurement : this.currentCumulativeDataMap.keySet())
  524.         {
  525.             if (!measurement.isPeriodic())
  526.             {
  527.                 map.put(measurement, getAggregateValue(measurement, this.overallGtuCount,
  528.                         this.getSimulator().getSimulatorAbsTime().minus(Time.ZERO)));
  529.             }
  530.         }
  531.         return map;
  532.     }

  533.     /**
  534.      * Returns a Table with loop detector positions.
  535.      * @param network RoadNetwork; network from which all detectors are found.
  536.      * @return Table; with loop detector positions.
  537.      */
  538.     public static Table asTablePositions(final RoadNetwork network)
  539.     {
  540.         Set<LoopDetector> detectors = getLoopDetectors(network);
  541.         Collection<Column<?>> columns = new LinkedHashSet<>();
  542.         columns.add(new Column<>("id", "detector id", String.class, null));
  543.         columns.add(new Column<>("laneId", "lane id", String.class, null));
  544.         columns.add(new Column<>("linkId", "link id", String.class, null));
  545.         columns.add(new Column<>("position", "detector position on the lane", Length.class, "m"));

  546.         return new Table("detectors", "list of all loop-detectors", columns)
  547.         {
  548.             /** {@inheritDoc} */
  549.             @Override
  550.             public Iterator<Row> iterator()
  551.             {
  552.                 Iterator<LoopDetector> iterator = detectors.iterator();
  553.                 return new Iterator<>()
  554.                 {
  555.                     /** {@inheritDoc} */
  556.                     @Override
  557.                     public boolean hasNext()
  558.                     {
  559.                         return iterator.hasNext();
  560.                     }

  561.                     /** {@inheritDoc} */
  562.                     @Override
  563.                     public Row next()
  564.                     {
  565.                         LoopDetector detector = iterator.next();
  566.                         return new Row(table(), new Object[] {detector.getId(), detector.getLane().getId(),
  567.                                 detector.getLane().getLink().getId(), detector.getLongitudinalPosition()});
  568.                     }
  569.                 };
  570.             }

  571.             /**
  572.              * Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
  573.              * @return Table; this table instance for inner classes.
  574.              */
  575.             private Table table()
  576.             {
  577.                 return this;
  578.             }

  579.             /** {@inheritDoc} */
  580.             @Override
  581.             public boolean isEmpty()
  582.             {
  583.                 return detectors.isEmpty();
  584.             }
  585.         };
  586.     }

  587.     /**
  588.      * Returns a Table with all periodic data, such as flow and speed per minute.
  589.      * @param network RoadNetwork; network from which all detectors are found.
  590.      * @return Table; with all periodic data, such as flow and speed per minute.
  591.      */
  592.     public static Table asTablePeriodicData(final RoadNetwork network)
  593.     {
  594.         Set<LoopDetector> detectors = getLoopDetectors(network);
  595.         Set<LoopDetectorMeasurement<?, ?>> measurements = getMeasurements(detectors, true);
  596.         Collection<Column<?>> columns = new LinkedHashSet<>();
  597.         columns.add(new Column<>("id", "detector id", String.class, null));
  598.         columns.add(new Column<>("t", "time (start of aggregation period)", Duration.class, "s"));
  599.         columns.add(new Column<>("q", "flow", Frequency.class, "/h"));
  600.         for (LoopDetectorMeasurement<?, ?> measurement : measurements)
  601.         {
  602.             columns.add(new Column<>(measurement.getName(), measurement.getDescription(), measurement.getValueType(),
  603.                     measurement.getUnit()));
  604.         }

  605.         return new Table("periodic", "periodic measurements", columns)
  606.         {
  607.             /** {@inheritDoc} */
  608.             @Override
  609.             public Iterator<Row> iterator()
  610.             {
  611.                 Iterator<LoopDetector> iterator = detectors.iterator();
  612.                 return new Iterator<>()
  613.                 {
  614.                     /** Index iterator. */
  615.                     private Iterator<Integer> indexIterator = Collections.emptyIterator();

  616.                     /** Current loop detector. */
  617.                     private LoopDetector loopDetector;

  618.                     /** Map of measurement data per measurement, updated for each detector. */
  619.                     private Map<LoopDetectorMeasurement<?, ?>, List<?>> map;

  620.                     /** {@inheritDoc} */
  621.                     @Override
  622.                     public boolean hasNext()
  623.                     {
  624.                         while (!this.indexIterator.hasNext())
  625.                         {
  626.                             if (!iterator.hasNext())
  627.                             {
  628.                                 return false;
  629.                             }
  630.                             this.loopDetector = iterator.next();
  631.                             this.loopDetector.aggregate();
  632.                             this.indexIterator = IntStream.range(0, this.loopDetector.flow.size()).iterator();
  633.                             this.map = this.loopDetector.periodicDataMap;
  634.                         }
  635.                         return true;
  636.                     }

  637.                     /** {@inheritDoc} */
  638.                     @Override
  639.                     public Row next()
  640.                     {
  641.                         Throw.when(!hasNext(), NoSuchElementException.class, "Periodic data unavailable.");
  642.                         Object[] data = new Object[columns.size()];
  643.                         int index = this.indexIterator.next();

  644.                         double t = this.loopDetector.firstAggregation.si + (index - 1) * this.loopDetector.aggregation.si;
  645.                         data[0] = this.loopDetector.getId();
  646.                         data[1] = Duration.instantiateSI(t < 0.0 ? 0.0 : t);
  647.                         data[2] = this.loopDetector.flow.get(index);
  648.                         int dataIndex = 3;
  649.                         for (LoopDetectorMeasurement<?, ?> measurement : measurements)
  650.                         {
  651.                             if (this.map.containsKey(measurement))
  652.                             {
  653.                                 data[dataIndex++] = this.map.get(measurement).get(index);
  654.                             }
  655.                             else
  656.                             {
  657.                                 // this data is not available for this detector
  658.                                 data[dataIndex++] = null;
  659.                             }
  660.                         }
  661.                         return new Row(table(), data);
  662.                     }
  663.                 };
  664.             }

  665.             /**
  666.              * Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
  667.              * @return Table; this table instance for inner classes.
  668.              */
  669.             private Table table()
  670.             {
  671.                 return this;
  672.             }

  673.             /** {@inheritDoc} */
  674.             @Override
  675.             public boolean isEmpty()
  676.             {
  677.                 return detectors.isEmpty() || !detectors.iterator().next().hasLastValue();
  678.             }
  679.         };
  680.     }

  681.     /**
  682.      * Returns a Table with all non-periodic data, such as vehicle passage times or platoon counts.
  683.      * @param network RoadNetwork; network from which all detectors are found.
  684.      * @return Table; with all non-periodic data, such as vehicle passage times or platoon counts.
  685.      */
  686.     public static Table asTableNonPeriodicData(final RoadNetwork network)
  687.     {
  688.         Set<LoopDetector> detectors = getLoopDetectors(network);
  689.         Set<LoopDetectorMeasurement<?, ?>> measurements = getMeasurements(detectors, false);
  690.         Collection<Column<?>> columns = new LinkedHashSet<>();
  691.         columns.add(new Column<>("id", "detector id", String.class, null));
  692.         columns.add(new Column<>("measurement", "measurement type", String.class, null));
  693.         columns.add(new Column<>("data", "data in any form", String.class, null));

  694.         return new Table("non-periodic", "non-periodic measurements", columns)
  695.         {
  696.             /** {@inheritDoc} */
  697.             @Override
  698.             public Iterator<Row> iterator()
  699.             {
  700.                 Iterator<LoopDetector> iterator = detectors.iterator();
  701.                 return new Iterator<>()
  702.                 {
  703.                     /** Index iterator. */
  704.                     private Iterator<LoopDetectorMeasurement<?, ?>> measurementIterator = Collections.emptyIterator();

  705.                     /** Current loop detector. */
  706.                     private LoopDetector loopDetector;

  707.                     /** Map of measurement data per measurement, updated for each detector. */
  708.                     private Map<LoopDetectorMeasurement<?, ?>, Object> map;

  709.                     /** Current measurement. */
  710.                     private LoopDetectorMeasurement<?, ?> measurement;

  711.                     /** {@inheritDoc} */
  712.                     @Override
  713.                     public boolean hasNext()
  714.                     {
  715.                         if (this.measurement != null)
  716.                         {
  717.                             return true; // catch consecutive 'hasNext' calls before next 'next' call
  718.                         }
  719.                         while (!this.measurementIterator.hasNext())
  720.                         {
  721.                             if (!iterator.hasNext())
  722.                             {
  723.                                 return false;
  724.                             }
  725.                             this.loopDetector = iterator.next();
  726.                             this.measurementIterator = measurements.iterator();
  727.                             this.map = this.loopDetector.getNonPeriodicMeasurements();
  728.                         }
  729.                         // skip if data is not available for this detector
  730.                         this.measurement = this.measurementIterator.next();
  731.                         if (!this.map.containsKey(this.measurement))
  732.                         {
  733.                             this.measurement = null;
  734.                             return hasNext();
  735.                         }
  736.                         return true;
  737.                     }

  738.                     /** {@inheritDoc} */
  739.                     @Override
  740.                     public Row next()
  741.                     {
  742.                         Throw.when(!hasNext(), NoSuchElementException.class, "Non-periodic data unavailable.");
  743.                         Object[] data = new Object[columns.size()];

  744.                         data[0] = this.loopDetector.getId();
  745.                         data[1] = this.measurement.getName();
  746.                         data[2] = this.map.get(this.measurement).toString();
  747.                         this.measurement = null;
  748.                         return new Row(table(), data);
  749.                     }
  750.                 };
  751.             }

  752.             /**
  753.              * Returns this table instance for inner classes as {@code Table.this} is not possible in an anonymous Table class.
  754.              * @return Table; this table instance for inner classes.
  755.              */
  756.             private Table table()
  757.             {
  758.                 return this;
  759.             }

  760.             /** {@inheritDoc} */
  761.             @Override
  762.             public boolean isEmpty()
  763.             {
  764.                 return detectors.isEmpty() || measurements.isEmpty();
  765.             }
  766.         };
  767.     }

  768.     /**
  769.      * Gathers all loop detectors from the network and puts them in a set sorted by loop detector id.
  770.      * @param network RoadNetwork; network.
  771.      * @return Set&lt;LoopDetector&gt;; set of loop detector sorted by loop detector id.
  772.      */
  773.     private static Set<LoopDetector> getLoopDetectors(final RoadNetwork network)
  774.     {
  775.         Set<LoopDetector> detectors = new TreeSet<>(new Comparator<LoopDetector>()
  776.         {
  777.             @Override
  778.             public int compare(final LoopDetector o1, final LoopDetector o2)
  779.             {
  780.                 return o1.getId().compareTo(o2.getId());
  781.             }
  782.         });
  783.         detectors.addAll(network.getObjectMap(LoopDetector.class).values().toCollection());
  784.         return detectors;
  785.     }

  786.     /**
  787.      * Returns all measurement type that are found accross a set of loop detectors.
  788.      * @param detectors Set&lt;LoopDetector&gt;; set of loop detectors.
  789.      * @param periodic boolean; gather the periodic measurements {@code true}, or the non-periodic measurements {@code false}.
  790.      * @return Set&lt;LoopDetectorMeasurement&lt;?, ?&gt;&gt;; set of periodic or non-periodic measurements from the detectors.
  791.      */
  792.     private static Set<LoopDetectorMeasurement<?, ?>> getMeasurements(final Set<LoopDetector> detectors, final boolean periodic)
  793.     {
  794.         return detectors.stream().flatMap((det) -> det.currentCumulativeDataMap.keySet().stream())
  795.                 .filter((measurement) -> measurement.isPeriodic() == periodic)
  796.                 .collect(Collectors.toCollection(LinkedHashSet::new));
  797.     }

  798.     /**
  799.      * Interface for what detectors measure.
  800.      * <p>
  801.      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  802.      * <br>
  803.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  804.      * </p>
  805.      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  806.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  807.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  808.      * @param <C> accumulated type
  809.      * @param <A> aggregated type
  810.      */
  811.     public interface LoopDetectorMeasurement<C, A>
  812.     {
  813.         /**
  814.          * Returns the initial value before accumulation.
  815.          * @return C; initial value before accumulation
  816.          */
  817.         C identity();

  818.         /**
  819.          * Returns an accumulated value for when the front reaches the detector. GTU's may trigger an exit without having
  820.          * triggered an entry due to a lane change. Reversely, GTU's may not trigger an exit while they did trigger an entry.
  821.          * @param cumulative C; accumulated value
  822.          * @param gtu LaneBasedGtu; gtu
  823.          * @param loopDetector Detector; loop detector
  824.          * @return C; accumulated value
  825.          */
  826.         C accumulateEntry(C cumulative, LaneBasedGtu gtu, LoopDetector loopDetector);

  827.         /**
  828.          * Returns an accumulated value for when the rear leaves the detector. GTU's may trigger an exit without having
  829.          * triggered an entry due to a lane change. Reversely, GTU's may not trigger an exit while they did trigger an entry.
  830.          * @param cumulative C; accumulated value
  831.          * @param gtu LaneBasedGtu; gtu
  832.          * @param loopDetector Detector; loop detector
  833.          * @return C; accumulated value
  834.          */
  835.         C accumulateExit(C cumulative, LaneBasedGtu gtu, LoopDetector loopDetector);

  836.         /**
  837.          * Returns whether the measurement aggregates every aggregation period (or only over the entire simulation).
  838.          * @return boolean; whether the measurement aggregates every aggregation period (or only over the entire simulation)
  839.          */
  840.         boolean isPeriodic();

  841.         /**
  842.          * Returns an aggregated value.
  843.          * @param cumulative C; accumulated value
  844.          * @param gtuCount int; GTU gtuCount
  845.          * @param aggregation Duration; aggregation period
  846.          * @return A; aggregated value
  847.          */
  848.         A aggregate(C cumulative, int gtuCount, Duration aggregation);

  849.         /**
  850.          * Returns the value name.
  851.          * @return String; value name
  852.          */
  853.         String getName();

  854.         /**
  855.          * Measurement description.
  856.          * @return String; measurement description.
  857.          */
  858.         String getDescription();

  859.         /**
  860.          * Returns the unit string, default is {@code null}.
  861.          * @return String; unit string.
  862.          */
  863.         default String getUnit()
  864.         {
  865.             return null;
  866.         }

  867.         /**
  868.          * Returns the data type.
  869.          * @return Class&lt;?&gt;; data type.
  870.          */
  871.         Class<?> getValueType();
  872.     }

  873.     /**
  874.      * Measurement of platoon sizes based on time between previous GTU exit and GTU entry.
  875.      * <p>
  876.      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  877.      * <br>
  878.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  879.      * </p>
  880.      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  881.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  882.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  883.      */
  884.     public static class PlatoonSizes implements LoopDetectorMeasurement<PlatoonMeasurement, List<Integer>>
  885.     {

  886.         /** Maximum time between two vehicles that are considered to be in the same platoon. */
  887.         private final Duration threshold;

  888.         /**
  889.          * Constructor.
  890.          * @param threshold Duration; maximum time between two vehicles that are considered to be in the same platoon
  891.          */
  892.         public PlatoonSizes(final Duration threshold)
  893.         {
  894.             this.threshold = threshold;
  895.         }

  896.         /** {@inheritDoc} */
  897.         @Override
  898.         public PlatoonMeasurement identity()
  899.         {
  900.             return new PlatoonMeasurement();
  901.         }

  902.         /** {@inheritDoc} */
  903.         @SuppressWarnings("synthetic-access")
  904.         @Override
  905.         public PlatoonMeasurement accumulateEntry(final PlatoonMeasurement cumulative, final LaneBasedGtu gtu,
  906.                 final LoopDetector loopDetector)
  907.         {
  908.             Time now = gtu.getSimulator().getSimulatorAbsTime();
  909.             if (now.si - cumulative.lastExitTime.si < this.threshold.si)
  910.             {
  911.                 cumulative.gtuCount++;
  912.             }
  913.             else
  914.             {
  915.                 if (cumulative.gtuCount > 0) // 0 means this is the first vehicle of the first platoon
  916.                 {
  917.                     cumulative.platoons.add(cumulative.gtuCount);
  918.                 }
  919.                 cumulative.gtuCount = 1;
  920.             }
  921.             cumulative.enteredGTUs.add(gtu);
  922.             cumulative.lastExitTime = now; // should we change lane before triggering the exit
  923.             return cumulative;
  924.         }

  925.         /** {@inheritDoc} */
  926.         @SuppressWarnings("synthetic-access")
  927.         @Override
  928.         public PlatoonMeasurement accumulateExit(final PlatoonMeasurement cumulative, final LaneBasedGtu gtu,
  929.                 final LoopDetector loopDetector)
  930.         {
  931.             int index = cumulative.enteredGTUs.indexOf(gtu);
  932.             if (index >= 0)
  933.             {
  934.                 cumulative.lastExitTime = gtu.getSimulator().getSimulatorAbsTime();
  935.                 // gtu is likely the oldest gtu in the list at index 0, but sometimes an older gtu may have left the detector by
  936.                 // changing lane, by clearing up to this gtu, older gtu's are automatically removed
  937.                 cumulative.enteredGTUs.subList(0, index).clear();
  938.             }
  939.             return cumulative;
  940.         }

  941.         /** {@inheritDoc} */
  942.         @Override
  943.         public boolean isPeriodic()
  944.         {
  945.             return false;
  946.         }

  947.         /** {@inheritDoc} */
  948.         @SuppressWarnings("synthetic-access")
  949.         @Override
  950.         public List<Integer> aggregate(final PlatoonMeasurement cumulative, final int count, final Duration aggregation)
  951.         {
  952.             if (cumulative.gtuCount > 0)
  953.             {
  954.                 cumulative.platoons.add(cumulative.gtuCount);
  955.                 cumulative.gtuCount = 0; // prevent that the last platoon is added again if the same output is saved again
  956.             }
  957.             return cumulative.platoons;
  958.         }

  959.         /** {@inheritDoc} */
  960.         @Override
  961.         public String getName()
  962.         {
  963.             return "platoon sizes";
  964.         }

  965.         /** {@inheritDoc} */
  966.         @Override
  967.         public String toString()
  968.         {
  969.             return getName();
  970.         }

  971.         /** {@inheritDoc} */
  972.         @Override
  973.         public String getDescription()
  974.         {
  975.             return "list of platoon sizes (threshold: " + this.threshold + ")";
  976.         }

  977.         /** {@inheritDoc} */
  978.         @Override
  979.         public Class<Integer> getValueType()
  980.         {
  981.             return Integer.class;
  982.         }

  983.     }

  984.     /**
  985.      * Cumulative information for platoon size measurement.
  986.      * <p>
  987.      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  988.      * <br>
  989.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  990.      * </p>
  991.      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  992.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  993.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  994.      */
  995.     static class PlatoonMeasurement
  996.     {
  997.         /** GTU's counted so far in the current platoon. */
  998.         private int gtuCount = 0;

  999.         /** Time the last GTU exited the detector. */
  1000.         private Time lastExitTime = Time.instantiateSI(Double.NEGATIVE_INFINITY);

  1001.         /** Stored sizes of earlier platoons. */
  1002.         private List<Integer> platoons = new ArrayList<>();

  1003.         /** GTU's currently on the detector, some may have left by a lane change. */
  1004.         private List<LaneBasedGtu> enteredGTUs = new ArrayList<>();
  1005.     }

  1006. }