View Javadoc
1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.Color;
4   import java.awt.event.ActionEvent;
5   import java.awt.event.ActionListener;
6   
7   import org.djunits.value.vdouble.scalar.Time;
8   import org.djutils.exceptions.Throw;
9   import org.jfree.chart.JFreeChart;
10  import org.jfree.chart.LegendItem;
11  import org.jfree.chart.LegendItemCollection;
12  import org.jfree.chart.axis.NumberAxis;
13  import org.jfree.chart.plot.XYPlot;
14  import org.jfree.data.DomainOrder;
15  import org.opentrafficsim.draw.BoundsPaintScale;
16  import org.opentrafficsim.draw.graphs.ContourDataSource.ContourDataType;
17  import org.opentrafficsim.draw.graphs.ContourDataSource.Dimension;
18  
19  /**
20   * Class for contour plots. The data that is plotted is stored in a {@code ContourDataSource}, which may be shared among several
21   * contour plots along the same path. This abstract class takes care of the interactions between the plot and the data pool. Sub
22   * classes only need to specify a few plot specific variables and functionalities.
23   * <p>
24   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
25   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
26   * </p>
27   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
28   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
29   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
30   * @param <Z> z-value type
31   */
32  public abstract class AbstractContourPlot<Z extends Number> extends AbstractSamplerPlot
33          implements XyInterpolatedDataset, ActionListener
34  {
35  
36      /** Color scale for the graph. */
37      private final BoundsPaintScale paintScale;
38  
39      /** Difference of successive values in the legend. */
40      private final Z legendStep;
41  
42      /** Format string used to create the captions in the legend. */
43      private final String legendFormat;
44  
45      /** Format string used to create status label (under the mouse). */
46      private final String valueFormat;
47  
48      /** Data pool. */
49      private final ContourDataSource dataPool;
50  
51      /** Block renderer in chart. */
52      private XyInterpolatedBlockRenderer blockRenderer = null;
53  
54      /**
55       * Constructor with specified paint scale.
56       * @param caption String; caption
57       * @param scheduler PlotScheduler; scheduler.
58       * @param dataPool ContourDataSource; data pool
59       * @param paintScale BoundsPaintScale; paint scale
60       * @param legendStep Z; increment between color legend entries
61       * @param legendFormat String; format string for the captions in the color legend
62       * @param valueFormat String; format string used to create status label (under the mouse)
63       */
64      public AbstractContourPlot(final String caption, final PlotScheduler scheduler, final ContourDataSource dataPool,
65              final BoundsPaintScale paintScale, final Z legendStep, final String legendFormat, final String valueFormat)
66      {
67          super(caption, dataPool.getUpdateInterval(), scheduler, dataPool.getSamplerData(), dataPool.getPath(),
68                  dataPool.getDelay());
69          dataPool.registerContourPlot(this);
70          this.dataPool = dataPool;
71          this.paintScale = paintScale;
72          this.legendStep = legendStep;
73          this.legendFormat = legendFormat;
74          this.valueFormat = valueFormat;
75          this.blockRenderer = new XyInterpolatedBlockRenderer(this);
76          this.blockRenderer.setPaintScale(this.paintScale);
77          this.blockRenderer.setBlockHeight(dataPool.getGranularity(Dimension.DISTANCE));
78          this.blockRenderer.setBlockWidth(dataPool.getGranularity(Dimension.TIME));
79          setChart(createChart());
80      }
81  
82      /**
83       * Constructor with default paint scale.
84       * @param caption String; caption
85       * @param scheduler PlotScheduler; scheduler.
86       * @param dataPool ContourDataSource; data pool
87       * @param legendStep Z; increment between color legend entries
88       * @param legendFormat String; format string for the captions in the color legend
89       * @param minValue Z; minimum value
90       * @param maxValue Z; maximum value
91       * @param valueFormat String; format string used to create status label (under the mouse)
92       */
93      @SuppressWarnings("parameternumber")
94      public AbstractContourPlot(final String caption, final PlotScheduler scheduler, final ContourDataSource dataPool,
95              final Z legendStep, final String legendFormat, final Z minValue, final Z maxValue, final String valueFormat)
96      {
97          this(caption, scheduler, dataPool, createPaintScale(minValue, maxValue), legendStep, legendFormat, valueFormat);
98      }
99  
100     /**
101      * Creates a default paint scale from red, via yellow to green.
102      * @param minValue Number; minimum value
103      * @param maxValue Number; maximum value
104      * @return BoundsPaintScale; default paint scale
105      */
106     private static BoundsPaintScale createPaintScale(final Number minValue, final Number maxValue)
107     {
108         Throw.when(minValue.doubleValue() >= maxValue.doubleValue(), IllegalArgumentException.class,
109                 "Minimum value %s is below or equal to maxumum value %s.", minValue, maxValue);
110         double[] boundaries =
111                 {minValue.doubleValue(), (minValue.doubleValue() + maxValue.doubleValue()) / 2.0, maxValue.doubleValue()};
112         Color[] colorValues = {Color.RED, Color.YELLOW, Color.GREEN};
113         return new BoundsPaintScale(boundaries, colorValues);
114     }
115 
116     /**
117      * Create a chart.
118      * @return JFreeChart; chart
119      */
120     private JFreeChart createChart()
121     {
122         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
123         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
124         XYPlot plot = new XYPlot(this, xAxis, yAxis, this.blockRenderer);
125         LegendItemCollection legend = new LegendItemCollection();
126         for (int i = 0;; i++)
127         {
128             double value = this.paintScale.getLowerBound() + i * this.legendStep.doubleValue();
129             if (value > this.paintScale.getUpperBound() + 1e-6)
130             {
131                 break;
132             }
133             legend.add(new LegendItem(String.format(this.legendFormat, scale(value)), this.paintScale.getPaint(value)));
134         }
135         legend.add(new LegendItem("No data", Color.BLACK));
136         plot.setFixedLegendItems(legend);
137         final JFreeChart chart = new JFreeChart(getCaption(), plot);
138         return chart;
139     }
140 
141     /**
142      * Returns the time granularity, just for information.
143      * @return double; time granularity
144      */
145     public double getTimeGranularity()
146     {
147         return this.dataPool.getGranularity(Dimension.TIME);
148     }
149 
150     /**
151      * Returns the space granularity, just for information.
152      * @return double; space granularity
153      */
154     public double getSpaceGranularity()
155     {
156         return this.dataPool.getGranularity(Dimension.DISTANCE);
157     }
158 
159     /**
160      * Sets the correct space granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots
161      * consistent.
162      * @param granularity double; space granularity
163      */
164     public final void setSpaceGranularity(final double granularity)
165     {
166         this.blockRenderer.setBlockHeight(granularity);
167         this.dataPool.spaceAxis.setGranularity(granularity); // XXX: AV added 16-5-2020
168     }
169 
170     /**
171      * Sets the correct time granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots
172      * consistent.
173      * @param granularity double; time granularity
174      */
175     public final void setTimeGranularity(final double granularity)
176     {
177         this.blockRenderer.setBlockWidth(granularity);
178         this.dataPool.timeAxis.setGranularity(granularity); // XXX: AV added 16-5-2020
179     }
180 
181     /**
182      * Sets the check box for interpolated rendering and block renderer setting. This is done from a {@code DataPool} to keep
183      * multiple plots consistent.
184      * @param interpolate boolean; selected or not
185      */
186     public final void setInterpolation(final boolean interpolate)
187     {
188         this.blockRenderer.setInterpolate(interpolate);
189         this.dataPool.timeAxis.setInterpolate(interpolate); // XXX: AV added 16-5-2020
190         this.dataPool.spaceAxis.setInterpolate(interpolate); // XXX: AV added 16-5-2020
191     }
192 
193     /**
194      * Returns the data pool for sub classes.
195      * @return ContourDataSource; data pool for subclasses
196      */
197     public final ContourDataSource getDataPool()
198     {
199         return this.dataPool;
200     }
201 
202     /** {@inheritDoc} */
203     @Override
204     public final int getItemCount(final int series)
205     {
206         return this.dataPool.getBinCount(Dimension.DISTANCE) * this.dataPool.getBinCount(Dimension.TIME);
207     }
208 
209     /** {@inheritDoc} */
210     @Override
211     public final Number getX(final int series, final int item)
212     {
213         return getXValue(series, item);
214     }
215 
216     /** {@inheritDoc} */
217     @Override
218     public final double getXValue(final int series, final int item)
219     {
220         return this.dataPool.getAxisValue(Dimension.TIME, item);
221     }
222 
223     /** {@inheritDoc} */
224     @Override
225     public final Number getY(final int series, final int item)
226     {
227         return getYValue(series, item);
228     }
229 
230     /** {@inheritDoc} */
231     @Override
232     public final double getYValue(final int series, final int item)
233     {
234         return this.dataPool.getAxisValue(Dimension.DISTANCE, item);
235     }
236 
237     /** {@inheritDoc} */
238     @Override
239     public final Number getZ(final int series, final int item)
240     {
241         return getZValue(series, item);
242     }
243 
244     /** {@inheritDoc} */
245     @Override
246     public final Comparable<String> getSeriesKey(final int series)
247     {
248         return getCaption();
249     }
250 
251     /** {@inheritDoc} */
252     @SuppressWarnings("rawtypes")
253     @Override
254     public final int indexOf(final Comparable seriesKey)
255     {
256         return 0;
257     }
258 
259     /** {@inheritDoc} */
260     @Override
261     public final DomainOrder getDomainOrder()
262     {
263         return DomainOrder.ASCENDING;
264     }
265 
266     /** {@inheritDoc} */
267     @Override
268     public final double getZValue(final int series, final int item)
269     {
270         // default 1 series
271         return getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME));
272     }
273 
274     /** {@inheritDoc} */
275     @Override
276     public final int getSeriesCount()
277     {
278         return 1; // default
279     }
280 
281     /** {@inheritDoc} */
282     @Override
283     public int getRangeBinCount()
284     {
285         return this.dataPool.getBinCount(Dimension.DISTANCE);
286     }
287 
288     /**
289      * Returns the status label when the mouse is over the given location.
290      * @param domainValue double; domain value (x-axis)
291      * @param rangeValue double; range value (y-axis)
292      * @return String; status label when the mouse is over the given location
293      */
294     @Override
295     public final String getStatusLabel(final double domainValue, final double rangeValue)
296     {
297         if (this.dataPool == null)
298         {
299             return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
300         }
301         int i = this.dataPool.getAxisBin(Dimension.DISTANCE, rangeValue);
302         int j = this.dataPool.getAxisBin(Dimension.TIME, domainValue);
303         int item = j * this.dataPool.getBinCount(Dimension.DISTANCE) + i;
304         double zValue = scale(
305                 getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME)));
306         return String.format("time %.0fs, distance %.0fm, " + this.valueFormat, domainValue, rangeValue, zValue);
307     }
308 
309     /** {@inheritDoc} */
310     @Override
311     protected final void increaseTime(final Time time)
312     {
313         if (this.dataPool != null) // dataPool is null at construction
314         {
315             this.dataPool.increaseTime(time);
316         }
317     }
318 
319     /**
320      * Obtain value for cell from the data pool.
321      * @param item int; item number
322      * @param cellLength double; cell length
323      * @param cellSpan double; cell duration
324      * @return double; value for cell from the data pool
325      */
326     protected abstract double getValue(int item, double cellLength, double cellSpan);
327 
328     /**
329      * Scale the value from SI to the desired unit for users.
330      * @param si double; SI value
331      * @return double; scaled value
332      */
333     protected abstract double scale(double si);
334 
335     /**
336      * Returns the contour data type for use in a {@code ContourDataSource}.
337      * @return CountorDataType; contour data type
338      */
339     protected abstract ContourDataType<Z, ?> getContourDataType();
340 
341     /**
342      * Returns the block renderer.
343      * @return block renderer
344      */
345     public XyInterpolatedBlockRenderer getBlockRenderer()
346     {
347         return this.blockRenderer;
348     }
349 
350     @Override
351     public final void actionPerformed(final ActionEvent actionEvent)
352     {
353         String command = actionEvent.getActionCommand();
354         if (command.equalsIgnoreCase("setSpaceGranularity"))
355         {
356             // The source field is abused to contain the granularity
357             double granularity = (double) actionEvent.getSource();
358             setSpaceGranularity(granularity);
359         }
360         else if (command.equalsIgnoreCase("setTimeGranularity"))
361         {
362             // The source field is abused to contain the granularity
363             double granularity = (double) actionEvent.getSource();
364             setTimeGranularity(granularity);
365         }
366         else
367         {
368             throw new RuntimeException("Unhandled ActionEvent (actionCommand is " + actionEvent.getActionCommand() + "\")");
369         }
370     }
371 
372 }