View Javadoc
1   package org.opentrafficsim.animation.graphs;
2   
3   import java.awt.Color;
4   
5   import org.djunits.value.vdouble.scalar.Duration;
6   import org.djunits.value.vdouble.scalar.Length;
7   import org.djutils.event.EventListener;
8   import org.djutils.event.EventType;
9   import org.djutils.exceptions.Throw;
10  import org.jfree.chart.JFreeChart;
11  import org.jfree.chart.LegendItem;
12  import org.jfree.chart.LegendItemCollection;
13  import org.jfree.chart.axis.NumberAxis;
14  import org.jfree.chart.plot.XYPlot;
15  import org.jfree.data.DomainOrder;
16  import org.opentrafficsim.animation.BoundsPaintScale;
17  import org.opentrafficsim.animation.graphs.AbstractContourPlot.ContourPaintState;
18  import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
19  import org.opentrafficsim.animation.graphs.ContourDataSource.ContourAdditionalDataType;
20  import org.opentrafficsim.animation.graphs.ContourDataSource.ContourDataType;
21  import org.opentrafficsim.animation.graphs.ContourDataSource.Dimension;
22  
23  /**
24   * Class for contour plots. The data that is plotted is stored in a {@code ContourDataSource}, which may be shared among several
25   * contour plots along the same path. This abstract class takes care of the interactions between the plot and the data source.
26   * Sub-classes only need to specify a few plot specific variables and functionalities.
27   * <p>
28   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
29   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
30   * </p>
31   * @author Alexander Verbraeck
32   * @author Peter Knoppers
33   * @author Wouter Schakel
34   * @param <Z> z-value type
35   */
36  public abstract class AbstractContourPlot<Z extends Number> extends AbstractSpaceTimePlot<ContourPaintState>
37          implements XyInterpolatedDataset
38  {
39  
40      /** Color scale for the graph. */
41      private final BoundsPaintScale paintScale;
42  
43      /** Label data. */
44      private final LabelData<Z> labelData;
45  
46      /** Data source. */
47      private final ContourDataSource source;
48  
49      /** Contour data type. */
50      private final ContourDataType<Z> contourDataType;
51  
52      /** Block renderer in chart. */
53      private XyInterpolatedBlockRenderer blockRenderer = null;
54  
55      /**
56       * Constructor with specified paint scale.
57       * @param caption caption
58       * @param source data source
59       * @param contourDataType contour data type
60       * @param paintScale paint scale
61       * @param labelData label data
62       */
63      public AbstractContourPlot(final String caption, final ContourDataSource source, final ContourDataType<Z> contourDataType,
64              final BoundsPaintScale paintScale, final LabelData<Z> labelData)
65      {
66          super(caption, source.getInitialUpdateInterval(), source.getPlotScheduler(), source.getDelay(),
67                  DEFAULT_INITIAL_UPPER_TIME_BOUND);
68          this.source = Throw.whenNull(source, "dataPool");
69          this.contourDataType = Throw.whenNull(contourDataType, "contourDataType");
70          this.paintScale = Throw.whenNull(paintScale, "paintScale");
71          this.labelData = Throw.whenNull(labelData, "labelData");
72          this.blockRenderer = new XyInterpolatedBlockRenderer(this);
73          this.blockRenderer.setPaintScale(this.paintScale);
74          this.blockRenderer.setBlockHeight(source.getGranularity(Dimension.DISTANCE));
75          this.blockRenderer.setBlockWidth(source.getGranularity(Dimension.TIME));
76          source.addPlot(this);
77          setChart(createChart());
78      }
79  
80      /**
81       * Constructor with default paint scale (red at minimum, yellow at mid-point, green at maximum).
82       * @param caption caption
83       * @param source data source
84       * @param contourDataType contour data type
85       * @param bounds paint scale bounds
86       * @param labelData label data
87       */
88      public AbstractContourPlot(final String caption, final ContourDataSource source,
89              final ContourAdditionalDataType<Z, ?> contourDataType, final Bounds<Z> bounds, final LabelData<Z> labelData)
90      {
91          this(caption, source, contourDataType, createPaintScale(bounds), labelData);
92      }
93  
94      /**
95       * Creates a default paint scale from red, via yellow to green.
96       * @param bounds paint scale bounds
97       * @return default paint scale
98       */
99      private static BoundsPaintScale createPaintScale(final Bounds<?> bounds)
100     {
101         Throw.when(bounds.minimum().doubleValue() >= bounds.maximum().doubleValue(), IllegalArgumentException.class,
102                 "Minimum value %s is above or equal to maximum value %s.", bounds.minimum(), bounds.maximum());
103         double[] boundaries = {bounds.minimum().doubleValue(),
104                 (bounds.minimum().doubleValue() + bounds.maximum().doubleValue()) / 2.0, bounds.maximum().doubleValue()};
105         Color[] colorValues = {Color.RED, Color.YELLOW, Color.GREEN};
106         return new BoundsPaintScale(boundaries, colorValues);
107     }
108 
109     /**
110      * Create a chart.
111      * @return chart
112      */
113     private JFreeChart createChart()
114     {
115         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
116         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
117         XYPlot plot = new XYPlot(this, xAxis, yAxis, this.blockRenderer);
118         LegendItemCollection legend = new LegendItemCollection();
119         for (int i = 0;; i++)
120         {
121             double value = this.paintScale.getLowerBound() + i * this.labelData.legendStep().doubleValue();
122             if (value > this.paintScale.getUpperBound() + 1e-6)
123             {
124                 break;
125             }
126             legend.add(new LegendItem(String.format(this.labelData.legendFormat(), scale(value)),
127                     this.paintScale.getPaint(value)));
128         }
129         legend.add(new LegendItem("No data", Color.BLACK));
130         plot.setFixedLegendItems(legend);
131         final JFreeChart chart = new JFreeChart(getCaption(), plot);
132         return chart;
133     }
134 
135     @Override
136     protected ContourPaintState emptyPaintState()
137     {
138         return new ContourPaintState(new float[0], 1.0, 1.0, 0, false, Duration.ZERO);
139     }
140 
141     /**
142      * Returns the contour data type for use in a {@link ContourDataSource}.
143      * @return contour data type
144      */
145     protected ContourDataType<Z> getContourDataType()
146     {
147         return this.contourDataType;
148     }
149 
150     /**
151      * Scale the z-value from SI to the desired unit for users, in line with unit in {@link LabelData#labelFormat}.
152      * @param si SI value
153      * @return scaled value
154      */
155     protected abstract double scale(double si);
156 
157     @Override
158     protected void setPaintState()
159     {
160         super.setPaintState();
161         this.blockRenderer.setInterpolate(getPaintState().interpolate());
162         this.blockRenderer.setBlockHeight(getPaintState().dx());
163         this.blockRenderer.setBlockWidth(getPaintState().dt());
164     }
165 
166     /**
167      * Returns the status label when the mouse is over the given location.
168      * @param domainValue domain value (x-axis)
169      * @param rangeValue range value (y-axis)
170      * @return status label when the mouse is over the given location
171      */
172     @Override
173     public String getStatusLabel(final double domainValue, final double rangeValue)
174     {
175         if (getPaintState().data().length == 0)
176         {
177             return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
178         }
179         int i = getPaintState().getSpaceSlice(rangeValue);
180         int j = getPaintState().getTimeSlice(domainValue);
181         int item = j * getPaintState().nSpaceSlices() + i;
182         double zValue = scale(getZValue(1, item));
183         return String.format("time %.0fs, distance %.0fm, " + this.labelData.labelFormat(), domainValue, rangeValue, zValue);
184     }
185 
186     // ===== XyInterpolatedDataset =====
187 
188     @Override
189     public int getItemCount(final int series)
190     {
191         return getPaintState().getItemCount();
192     }
193 
194     @Override
195     public Number getX(final int series, final int item)
196     {
197         return getXValue(series, item);
198     }
199 
200     @Override
201     public double getXValue(final int series, final int item)
202     {
203         return getPaintState().getTimeValue(item);
204     }
205 
206     @Override
207     public Number getY(final int series, final int item)
208     {
209         return getYValue(series, item);
210     }
211 
212     @Override
213     public double getYValue(final int series, final int item)
214     {
215         return getPaintState().getSpaceValue(item);
216     }
217 
218     @Override
219     public Number getZ(final int series, final int item)
220     {
221         return getZValue(series, item);
222     }
223 
224     @Override
225     public Comparable<String> getSeriesKey(final int series)
226     {
227         return getCaption();
228     }
229 
230     @SuppressWarnings("rawtypes")
231     @Override
232     public int indexOf(final Comparable seriesKey)
233     {
234         return 0;
235     }
236 
237     @Override
238     public DomainOrder getDomainOrder()
239     {
240         return DomainOrder.ASCENDING;
241     }
242 
243     @Override
244     public double getZValue(final int series, final int item)
245     {
246         // default 1 series
247         return getPaintState().data()[item];
248     }
249 
250     @Override
251     public int getSeriesCount()
252     {
253         return 1; // default
254     }
255 
256     @Override
257     public int getRangeBinCount()
258     {
259         return getPaintState().nSpaceSlices();
260     }
261 
262     // ===== Delegate forwards =====
263 
264     /**
265      * Add listener to synchronize UI element when another UI element changes a setting, or the setting is changed
266      * programmatically.
267      * @param listener listener
268      * @param eventType event type (i.e. {@code ContourDataSource.GRANULARITY}, {@code ContourDataSource.INTERPOLATE} or
269      *            {@code ContourDataSource.SMOOTH})
270      */
271     public void addListener(final EventListener listener, final EventType eventType)
272     {
273         this.source.addListener(listener, eventType);
274     }
275 
276     /**
277      * Returns the available granularities that a linked plot may use.
278      * @param dimension space or time
279      * @return available granularities that a linked plot may use
280      */
281     public double[] getGranularities(final Dimension dimension)
282     {
283         return this.source.getGranularities(dimension);
284     }
285 
286     /**
287      * Returns the selected granularity that a linked plot should use.
288      * @param dimension space or time
289      * @return granularity that a linked plot should use
290      */
291     public double getGranularity(final Dimension dimension)
292     {
293         return this.source.getGranularity(dimension);
294     }
295 
296     /**
297      * Sets the granularity of the plot. This will invalidate the plot triggering a redraw.
298      * @param dimension space or time
299      * @param granularity granularity in space or time (SI unit)
300      */
301     public void setGranularity(final Dimension dimension, final double granularity)
302     {
303         this.source.setGranularity(dimension, granularity);
304         invalidate();
305     }
306 
307     /**
308      * Sets bi-linear interpolation enabled or disabled. This will invalidate the plot triggering a redraw.
309      * @param interpolate whether to enable interpolation
310      */
311     public void setInterpolate(final boolean interpolate)
312     {
313         this.source.setInterpolate(interpolate);
314         invalidate();
315     }
316 
317     /**
318      * Sets the adaptive smoothing enabled or disabled. This will invalidate the plot triggering a redraw.
319      * @param smooth whether to smooth the plot
320      */
321     public void setSmooth(final boolean smooth)
322     {
323         this.source.setSmooth(smooth);
324         invalidate();
325     }
326 
327     @Override
328     protected void calculatePaintState(final Duration time)
329     {
330         this.source.calculatePaintStateSafe(time);
331     }
332 
333     @Override
334     protected Length getEndLocation()
335     {
336         return this.source.getPath().getTotalLength();
337     }
338 
339     // ===== Helper classes =====
340 
341     /**
342      * Bounds for the color bounds scale.
343      * @param minimum minimum value
344      * @param maximum maximum value
345      * @param <Z> value type
346      */
347     public record Bounds<Z extends Number>(Z minimum, Z maximum)
348     {
349     }
350 
351     /**
352      * Input container for label input.
353      * @param legendStep increment between color legend entries
354      * @param legendFormat format string for the captions in the color legend, for example {@code %.1fm/s} for {@code 30.2m/s}
355      * @param labelFormat format string used to create status label (under the mouse), e.g. {@code desired speed %.1f m/s}
356      * @param <Z> value type
357      */
358     public record LabelData<Z extends Number>(Z legendStep, String legendFormat, String labelFormat)
359     {
360     }
361 
362     /**
363      * Paint state for contour plots.
364      * @param data data as flat array
365      * @param dx space granularity
366      * @param dt time granularity
367      * @param nSpaceSlices number of slices for space
368      * @param interpolate whether to interpolate
369      * @param getAvailableTime available time
370      */
371     public record ContourPaintState(float[] data, double dx, double dt, int nSpaceSlices, boolean interpolate,
372             Duration getAvailableTime) implements PaintState
373     {
374 
375         /**
376          * Returns the number of available items.
377          * @return number of available items
378          */
379         public int getItemCount()
380         {
381             return data().length;
382         }
383 
384         /**
385          * Returns the time slice number of the item.
386          * @param item item number
387          * @return time slice number of the item
388          */
389         public double getTimeValue(final int item)
390         {
391             return dt() * (item / nSpaceSlices());
392         }
393 
394         /**
395          * Returns the space slice number of the item.
396          * @param item item number
397          * @return space slice number of the item
398          */
399         public double getSpaceValue(final int item)
400         {
401             return dx() * (item % nSpaceSlices());
402         }
403 
404         /**
405          * Returns the time slice index for the given time value.
406          * @param t time value
407          * @return time slice index
408          */
409         public int getTimeSlice(final double t)
410         {
411             int n = (int) (t / dt());
412             int nMax = data().length / nSpaceSlices();
413             return n < 0 ? 0 : (n > nMax ? nMax : n);
414         }
415 
416         /**
417          * Returns the space slice index for the given space value.
418          * @param x space value
419          * @return space slice index
420          */
421         public int getSpaceSlice(final double x)
422         {
423             int n = (int) (x / dx());
424             return n < 0 ? 0 : (n > nSpaceSlices() ? nSpaceSlices() : n);
425         }
426 
427     }
428 
429 }