View Javadoc
1   package org.opentrafficsim.animation.graphs;
2   
3   import java.util.Optional;
4   
5   import org.djunits.value.vdouble.scalar.Duration;
6   import org.djutils.exceptions.Throw;
7   import org.jfree.chart.JFreeChart;
8   import org.jfree.chart.axis.NumberAxis;
9   import org.jfree.chart.plot.XYPlot;
10  import org.jfree.chart.renderer.xy.XYBarRenderer;
11  import org.jfree.data.DomainOrder;
12  import org.jfree.data.xy.IntervalXYDataset;
13  import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
14  import org.opentrafficsim.animation.graphs.DistributionPlotExtendedData.DistributionPaintState;
15  import org.opentrafficsim.animation.graphs.GraphPath.Section;
16  import org.opentrafficsim.kpi.interfaces.GtuData;
17  import org.opentrafficsim.kpi.interfaces.LaneData;
18  import org.opentrafficsim.kpi.sampling.SamplerData;
19  import org.opentrafficsim.kpi.sampling.Trajectory;
20  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
21  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
22  
23  /**
24   * Distribution data plot for any numerical extended trajectory data. Data is collected cumulatively; each interval adds to the
25   * totals of the previous interval.
26   * <p>
27   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
28   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
29   * </p>
30   * @author Wouter Schakel
31   * @param <G> GTU data type that needs to be consistent between the sampler data and the extended data type
32   * @param <Z> value type that needs to be consistent between extended data type and label data
33   */
34  public class DistributionPlotExtendedData<G extends GtuData, Z extends Number> extends AbstractPlot<DistributionPaintState>
35          implements IntervalXYDataset
36  {
37  
38      /** Sampler data. */
39      private final SamplerData<G> samplerData;
40  
41      /** Lanes registered in the sampler. */
42      private final GraphPath<? extends LaneData<?>> path;
43  
44      /** Data type. */
45      private final ExtendedDataType<Z, ?, ?, ?> dataType;
46  
47      /** Time of most recent update. */
48      private double lastUpdateTime = Double.NEGATIVE_INFINITY;
49  
50      /** X-values. */
51      private final double[] x;
52  
53      /** Step size. */
54      private final double dx;
55  
56      /** Y-values. */
57      private final long[] y;
58  
59      /**
60       * Constructor.
61       * @param samplerData sampler data
62       * @param path path
63       * @param dataType data type
64       * @param plotScheduler plot scheduler
65       * @param labelData label data
66       */
67      public DistributionPlotExtendedData(final SamplerData<G> samplerData, final GraphPath<? extends LaneData<?>> path,
68              final ExtendedDataType<Z, ?, ?, G> dataType, final PlotScheduler plotScheduler, final LabelData<Z> labelData)
69      {
70          super(plotScheduler, labelData.caption(), Duration.ofSI(10.0), Duration.ZERO);
71          int n = 1 + (int) Math.round(
72                  (labelData.maximum().doubleValue() - labelData.minimum().doubleValue()) / labelData.step().doubleValue());
73          this.x = new double[n];
74          for (int i = 0; i < n; i++)
75          {
76              this.x[i] = labelData.minimum().doubleValue() + i * labelData.step().doubleValue();
77          }
78          this.dx = labelData.step().doubleValue();
79          this.y = new long[n];
80          this.samplerData = Throw.whenNull(samplerData, "samplerData");
81          this.path = Throw.whenNull(path, "path");
82          this.dataType = Throw.whenNull(dataType, "dataType");
83          setChart(createChart(labelData.xLabel()));
84      }
85  
86      /**
87       * Create a chart.
88       * @param xLabel label on x-axis
89       * @return JFreeChart; chart
90       */
91      private JFreeChart createChart(final String xLabel)
92      {
93          NumberAxis xAxis = new NumberAxis(xLabel);
94          xAxis.setRange(this.x[0], this.x[this.x.length - 1]);
95          NumberAxis yAxis = new NumberAxis("Count [-]");
96          yAxis.setAutoRangeIncludesZero(true);
97          XYBarRenderer renderer = new XYBarRenderer();
98          XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
99          return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, false);
100     }
101 
102     @Override
103     public int getSeriesCount()
104     {
105         return 1;
106     }
107 
108     @SuppressWarnings("rawtypes")
109     @Override
110     public Comparable getSeriesKey(final int series)
111     {
112         return Integer.valueOf(1);
113     }
114 
115     @SuppressWarnings("rawtypes")
116     @Override
117     public int indexOf(final Comparable seriesKey)
118     {
119         return 0;
120     }
121 
122     @Override
123     public DomainOrder getDomainOrder()
124     {
125         return DomainOrder.ASCENDING;
126     }
127 
128     @Override
129     public int getItemCount(final int series)
130     {
131         return this.x.length - 1;
132     }
133 
134     @Override
135     public Number getX(final int series, final int item)
136     {
137         return this.x[item];
138     }
139 
140     @Override
141     public double getXValue(final int series, final int item)
142     {
143         return this.x[item];
144     }
145 
146     @Override
147     public Number getY(final int series, final int item)
148     {
149         return this.y[item];
150     }
151 
152     @Override
153     public double getYValue(final int series, final int item)
154     {
155         return this.y[item];
156     }
157 
158     @Override
159     public Number getStartX(final int series, final int item)
160     {
161         return this.x[item];
162     }
163 
164     @Override
165     public double getStartXValue(final int series, final int item)
166     {
167         return this.x[item];
168     }
169 
170     @Override
171     public Number getEndX(final int series, final int item)
172     {
173         return this.x[item + 1];
174     }
175 
176     @Override
177     public double getEndXValue(final int series, final int item)
178     {
179         return this.x[item + 1];
180     }
181 
182     @Override
183     public Number getStartY(final int series, final int item)
184     {
185         return getYValue(series, item);
186     }
187 
188     @Override
189     public double getStartYValue(final int series, final int item)
190     {
191         return getYValue(series, item);
192     }
193 
194     @Override
195     public Number getEndY(final int series, final int item)
196     {
197         return getYValue(series, item);
198     }
199 
200     @Override
201     public double getEndYValue(final int series, final int item)
202     {
203         return getYValue(series, item);
204     }
205 
206     @Override
207     public GraphType getGraphType()
208     {
209         return GraphType.OTHER;
210     }
211 
212     @Override
213     public String getStatusLabel(final double domainValue, final double rangeValue)
214     {
215         return " ";
216     }
217 
218     @Override
219     protected DistributionPaintState emptyPaintState()
220     {
221         return new DistributionPaintState(Duration.ZERO);
222     }
223 
224     @Override
225     protected void calculatePaintState(final Duration time)
226     {
227         for (Section<? extends LaneData<?>> section : this.path.getSections())
228         {
229             for (LaneData<?> lane : section.sections())
230             {
231                 processLane(lane);
232             }
233         }
234         this.lastUpdateTime = time.si;
235         offerPaintState(new DistributionPaintState(time));
236     }
237 
238     /**
239      * Processes a single lane while updating the time.
240      * @param lane lane
241      */
242     private void processLane(final LaneData<?> lane)
243     {
244         Optional<TrajectoryGroup<G>> trajectories = this.samplerData.getTrajectoryGroup(lane);
245         if (trajectories.isPresent())
246         {
247             for (Trajectory<G> trajectory : trajectories.get())
248             {
249                 int n = trajectory.size() - 1;
250                 while (n >= 0 && trajectory.getT(n) > this.lastUpdateTime)
251                 {
252                     Z z = trajectory.getExtendedData(this.dataType, n);
253                     double value = z == null ? Double.NaN : z.doubleValue();
254                     if (!Double.isNaN(value) && value >= this.x[0] && value < this.x[this.x.length - 1])
255                     {
256                         int index = (int) Math.floor((value - this.x[0]) / this.dx);
257                         this.y[index]++;
258                     }
259                     n--;
260                 }
261             }
262         }
263     }
264 
265     @Override
266     public void setAutoBoundDomain(final XYPlot plot)
267     {
268         // don't need to do anything, override to prevent exception thrown in super
269     }
270 
271     @Override
272     public void setAutoBoundRange(final XYPlot plot)
273     {
274         // don't need to do anything, override to prevent exception thrown in super
275     }
276 
277     /**
278      * Input container for label input.
279      * @param <Z> value type
280      * @param caption caption
281      * @param xLabel label on x-axis
282      * @param minimum minimum x-value
283      * @param step step value
284      * @param maximum maximum x-value
285      */
286     public record LabelData<Z extends Number>(String caption, String xLabel, Z minimum, Z step, Z maximum)
287     {
288     	
289         /**
290          * Constructor.
291          * @param caption caption
292          * @param xLabel label on x-axis
293          * @param minimum minimum x-value
294          * @param step step value
295          * @param maximum maximum x-value
296          */
297         public LabelData
298         {
299             Throw.when(maximum.doubleValue() <= minimum.doubleValue(), IllegalArgumentException.class,
300                     "maximum must be greater than minimum");
301             Throw.when(step.doubleValue() <= 0.0, IllegalArgumentException.class, "step must be greater than 0");
302         }
303         
304     }
305 
306     /**
307      * Only contains time. The y-values are cumulative so direct internal storage suffices.
308      * @param getAvailableTime available time
309      */
310     public record DistributionPaintState(Duration getAvailableTime) implements PaintState
311     {
312     }
313 
314 }