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