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   import java.util.LinkedHashMap;
7   import java.util.Map;
8   
9   import javax.swing.ButtonGroup;
10  import javax.swing.JCheckBoxMenuItem;
11  import javax.swing.JMenu;
12  import javax.swing.JPopupMenu;
13  import javax.swing.JRadioButtonMenuItem;
14  
15  import org.djunits.value.vdouble.scalar.Time;
16  import org.djutils.exceptions.Throw;
17  import org.jfree.chart.JFreeChart;
18  import org.jfree.chart.LegendItem;
19  import org.jfree.chart.LegendItemCollection;
20  import org.jfree.chart.axis.NumberAxis;
21  import org.jfree.chart.plot.XYPlot;
22  import org.jfree.data.DomainOrder;
23  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
24  import org.opentrafficsim.draw.core.BoundsPaintScale;
25  import org.opentrafficsim.draw.graphs.ContourDataSource.ContourDataType;
26  import org.opentrafficsim.draw.graphs.ContourDataSource.Dimension;
27  
28  /**
29   * Class for contour plots. The data that is plotted is stored in a {@code ContourDataSource}, which may be shared among several
30   * contour plots along the same path. This abstract class takes care of the interactions between the plot and the data pool. Sub
31   * classes only need to specify a few plot specific variables and functionalities.
32   * <p>
33   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
34   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
35   * <p>
36   * @version $Revision$, $LastChangedDate$, by $Author$, initial version 4 okt. 2018 <br>
37   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
38   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
39   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
40   * @param <Z> z-value type
41   */
42  public abstract class AbstractContourPlot<Z extends Number> extends AbstractSamplerPlot implements XYInterpolatedDataset
43  {
44  
45      /** */
46      private static final long serialVersionUID = 20181004L;
47  
48      /** Color scale for the graph. */
49      private final BoundsPaintScale paintScale;
50  
51      /** Difference of successive values in the legend. */
52      private final Z legendStep;
53  
54      /** Format string used to create the captions in the legend. */
55      private final String legendFormat;
56  
57      /** Format string used to create status label (under the mouse). */
58      private final String valueFormat;
59  
60      /** Data pool. */
61      private final ContourDataSource<?> dataPool;
62  
63      /** Map to set time granularity. */
64      private Map<JRadioButtonMenuItem, Double> timeGranularityButtons = new LinkedHashMap<>();
65  
66      /** Map to set space granularity. */
67      private Map<JRadioButtonMenuItem, Double> spaceGranularityButtons = new LinkedHashMap<>();
68  
69      /** Check box for smoothing. */
70      private JCheckBoxMenuItem smoothCheckBox;
71  
72      /** Check box for interpolation. */
73      private JCheckBoxMenuItem interpolateCheckBox;
74  
75      /** Block renderer in chart. */
76      private XYInterpolatedBlockRenderer blockRenderer = null;
77  
78      /**
79       * Constructor with specified paint scale.
80       * @param caption String; caption
81       * @param simulator OTSSimulatorInterface; simulator
82       * @param dataPool ContourDataSource&lt;?&gt;; data pool
83       * @param paintScale BoundsPaintScale; paint scale
84       * @param legendStep Z; increment between color legend entries
85       * @param legendFormat String; format string for the captions in the color legend
86       * @param valueFormat String; format string used to create status label (under the mouse)
87       */
88      public AbstractContourPlot(final String caption, final OTSSimulatorInterface simulator, final ContourDataSource<?> dataPool,
89              final BoundsPaintScale paintScale, final Z legendStep, final String legendFormat, final String valueFormat)
90      {
91          super(caption, dataPool.getUpdateInterval(), simulator, dataPool.getSampler(), dataPool.getPath(), dataPool.getDelay());
92          dataPool.registerContourPlot(this);
93          this.dataPool = dataPool;
94          this.paintScale = paintScale;
95          this.legendStep = legendStep;
96          this.legendFormat = legendFormat;
97          this.valueFormat = valueFormat;
98          this.blockRenderer = new XYInterpolatedBlockRenderer(this);
99          this.blockRenderer.setPaintScale(this.paintScale);
100         this.blockRenderer.setBlockHeight(dataPool.getGranularity(Dimension.DISTANCE));
101         this.blockRenderer.setBlockWidth(dataPool.getGranularity(Dimension.TIME));
102         setChart(createChart());
103     }
104 
105     /**
106      * Constructor with default paint scale.
107      * @param caption String; caption
108      * @param simulator OTSSimulatorInterface; simulator
109      * @param dataPool ContourDataSource&lt;?&gt;; data pool
110      * @param legendStep Z; increment between color legend entries
111      * @param legendFormat String; format string for the captions in the color legend
112      * @param minValue Z; minimum value
113      * @param maxValue Z; maximum value
114      * @param valueFormat String; format string used to create status label (under the mouse)
115      */
116     @SuppressWarnings("parameternumber")
117     public AbstractContourPlot(final String caption, final OTSSimulatorInterface simulator, final ContourDataSource<?> dataPool,
118             final Z legendStep, final String legendFormat, final Z minValue, final Z maxValue, final String valueFormat)
119     {
120         this(caption, simulator, dataPool, createPaintScale(minValue, maxValue), legendStep, legendFormat, valueFormat);
121     }
122 
123     /**
124      * Creates a default paint scale from red, via yellow to green.
125      * @param minValue Number; minimum value
126      * @param maxValue Number; maximum value
127      * @return BoundsPaintScale; default paint scale
128      */
129     private static BoundsPaintScale createPaintScale(final Number minValue, final Number maxValue)
130     {
131         Throw.when(minValue.doubleValue() >= maxValue.doubleValue(), IllegalArgumentException.class,
132                 "Minimum value %s is below or equal to maxumum value %s.", minValue, maxValue);
133         double[] boundaries =
134                 {minValue.doubleValue(), (minValue.doubleValue() + maxValue.doubleValue()) / 2.0, maxValue.doubleValue()};
135         Color[] colorValues = {Color.RED, Color.YELLOW, Color.GREEN};
136         return new BoundsPaintScale(boundaries, colorValues);
137     }
138 
139     /**
140      * Create a chart.
141      * @return JFreeChart; chart
142      */
143     private JFreeChart createChart()
144     {
145         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
146         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
147         XYPlot plot = new XYPlot(this, xAxis, yAxis, this.blockRenderer);
148         LegendItemCollection legend = new LegendItemCollection();
149         for (int i = 0;; i++)
150         {
151             double value = this.paintScale.getLowerBound() + i * this.legendStep.doubleValue();
152             if (value > this.paintScale.getUpperBound() + 1e-6)
153             {
154                 break;
155             }
156             legend.add(new LegendItem(String.format(this.legendFormat, scale(value)), this.paintScale.getPaint(value)));
157         }
158         legend.add(new LegendItem("No data", Color.BLACK));
159         plot.setFixedLegendItems(legend);
160         final JFreeChart chart = new JFreeChart(getCaption(), plot);
161         return chart;
162     }
163 
164     /** {@inheritDoc} */
165     @Override
166     protected void addPopUpMenuItems(final JPopupMenu popupMenu)
167     {
168         super.addPopUpMenuItems(popupMenu);
169         JMenu spaceGranularityMenu = buildMenu("Distance granularity", "%.0f m", 1000, "%.0f km", "setSpaceGranularity",
170                 this.dataPool.getGranularities(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.DISTANCE),
171                 this.spaceGranularityButtons);
172         popupMenu.insert(spaceGranularityMenu, 0);
173         JMenu timeGranularityMenu = buildMenu("Time granularity", "%.0f s", 60.0, "%.0f min", "setTimeGranularity",
174                 this.dataPool.getGranularities(Dimension.TIME), this.dataPool.getGranularity(Dimension.TIME),
175                 this.timeGranularityButtons);
176         popupMenu.insert(timeGranularityMenu, 1);
177         this.smoothCheckBox = new JCheckBoxMenuItem("Adaptive smoothing method", false);
178         this.smoothCheckBox.addActionListener(new ActionListener()
179         {
180             /** {@inheritDoc} */
181             @SuppressWarnings("synthetic-access")
182             @Override
183             public void actionPerformed(final ActionEvent e)
184             {
185                 AbstractContourPlot.this.dataPool.setSmooth(((JCheckBoxMenuItem) e.getSource()).isSelected());
186                 notifyPlotChange();
187             }
188         });
189         popupMenu.insert(this.smoothCheckBox, 2);
190         this.interpolateCheckBox = new JCheckBoxMenuItem("Bilinear interpolation", true);
191         this.interpolateCheckBox.addActionListener(new ActionListener()
192         {
193             /** {@inheritDoc} */
194             @SuppressWarnings("synthetic-access")
195             @Override
196             public void actionPerformed(final ActionEvent e)
197             {
198                 boolean interpolate = ((JCheckBoxMenuItem) e.getSource()).isSelected();
199                 AbstractContourPlot.this.blockRenderer.setInterpolate(interpolate);
200                 AbstractContourPlot.this.dataPool.setInterpolate(interpolate);
201                 notifyPlotChange();
202             }
203         });
204         popupMenu.insert(this.interpolateCheckBox, 3);
205     }
206 
207     /**
208      * Create a JMenu to let the user set the granularity.
209      * @param menuName String; caption for the new JMenu
210      * @param format1 String; format string for the values in the items under the new JMenu, below formatValue
211      * @param formatValue double; format value
212      * @param format2 String; format string for the values in the items under the new JMenu, above and equal to formatValue
213      * @param command String; prefix for the actionCommand of the items under the new JMenu
214      * @param values double[]; array of values to be formatted using the format strings to yield the items under the new JMenu
215      * @param initialValue double; the currently selected value (used to put the bullet on the correct item)
216      * @param granularityButtons Map&lt;JRadioButtonMenuItem, Double&gt;; map in to which buttons should be added
217      * @return JMenu with JRadioMenuItems for the values and a bullet on the currentValue item
218      */
219     private JMenu buildMenu(final String menuName, final String format1, final double formatValue, final String format2,
220             final String command, final double[] values, final double initialValue,
221             final Map<JRadioButtonMenuItem, Double> granularityButtons)
222     {
223         JMenu result = new JMenu(menuName);
224         ButtonGroup group = new ButtonGroup();
225         for (double value : values)
226         {
227             JRadioButtonMenuItem item = new JRadioButtonMenuItem(
228                     String.format(value < formatValue ? format1 : format2, value < formatValue ? value : value / formatValue));
229             granularityButtons.put(item, value);
230             item.setSelected(value == initialValue);
231             item.setActionCommand(command);
232             item.addActionListener(new ActionListener()
233             {
234                 /** {@inheritDoc} */
235                 @SuppressWarnings("synthetic-access")
236                 @Override
237                 public void actionPerformed(final ActionEvent actionEvent)
238                 {
239                     if (command.equalsIgnoreCase("setSpaceGranularity"))
240                     {
241                         double granularity = AbstractContourPlot.this.spaceGranularityButtons.get(actionEvent.getSource());
242                         AbstractContourPlot.this.dataPool.setGranularity(Dimension.DISTANCE, granularity);
243                     }
244                     else if (command.equalsIgnoreCase("setTimeGranularity"))
245                     {
246                         double granularity = AbstractContourPlot.this.timeGranularityButtons.get(actionEvent.getSource());
247                         AbstractContourPlot.this.dataPool.setGranularity(Dimension.TIME, granularity);
248                     }
249                     else
250                     {
251                         throw new RuntimeException("Unknown ActionEvent");
252                     }
253                 }
254             });
255             result.add(item);
256             group.add(item);
257         }
258         return result;
259     }
260 
261     /**
262      * Returns the time granularity, just for information.
263      * @return double; time granularity
264      */
265     public double getTimeGranularity()
266     {
267         return this.dataPool.getGranularity(Dimension.TIME);
268     }
269 
270     /**
271      * Returns the space granularity, just for information.
272      * @return double; space granularity
273      */
274     public double getSpaceGranularity()
275     {
276         return this.dataPool.getGranularity(Dimension.DISTANCE);
277     }
278 
279     /**
280      * Sets the correct space granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots
281      * consistent.
282      * @param granularity double; space granularity
283      */
284     protected final void setSpaceGranularityRadioButton(final double granularity)
285     {
286         this.blockRenderer.setBlockHeight(granularity);
287         for (JRadioButtonMenuItem button : this.spaceGranularityButtons.keySet())
288         {
289             button.setSelected(this.spaceGranularityButtons.get(button) == granularity);
290         }
291     }
292 
293     /**
294      * Sets the correct time granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots
295      * consistent.
296      * @param granularity double; time granularity
297      */
298     protected final void setTimeGranularityRadioButton(final double granularity)
299     {
300         this.blockRenderer.setBlockWidth(granularity);
301         for (JRadioButtonMenuItem button : this.timeGranularityButtons.keySet())
302         {
303             button.setSelected(this.timeGranularityButtons.get(button) == granularity);
304         }
305     }
306 
307     /**
308      * Sets the check box for smooth rendering. This is done from a {@code DataPool} to keep multiple plots consistent.
309      * @param smooth boolean; selected or not
310      */
311     protected final void setSmoothing(final boolean smooth)
312     {
313         this.smoothCheckBox.setSelected(smooth);
314     }
315 
316     /**
317      * Sets the check box for interpolated rendering and block renderer setting. This is done from a {@code DataPool} to keep
318      * multiple plots consistent.
319      * @param interpolate boolean; selected or not
320      */
321     protected final void setInterpolation(final boolean interpolate)
322     {
323         this.blockRenderer.setInterpolate(interpolate);
324         this.interpolateCheckBox.setSelected(interpolate);
325     }
326 
327     /**
328      * Returns the data pool for sub classes.
329      * @return ContourDataSource; data pool for subclasses
330      */
331     protected final ContourDataSource<?> getDataPool()
332     {
333         return this.dataPool;
334     }
335 
336     /** {@inheritDoc} */
337     @Override
338     public final int getItemCount(final int series)
339     {
340         return this.dataPool.getBinCount(Dimension.DISTANCE) * this.dataPool.getBinCount(Dimension.TIME);
341     }
342 
343     /** {@inheritDoc} */
344     @Override
345     public final Number getX(final int series, final int item)
346     {
347         return getXValue(series, item);
348     }
349 
350     /** {@inheritDoc} */
351     @Override
352     public final double getXValue(final int series, final int item)
353     {
354         return this.dataPool.getAxisValue(Dimension.TIME, item);
355     }
356 
357     /** {@inheritDoc} */
358     @Override
359     public final Number getY(final int series, final int item)
360     {
361         return getYValue(series, item);
362     }
363 
364     /** {@inheritDoc} */
365     @Override
366     public final double getYValue(final int series, final int item)
367     {
368         return this.dataPool.getAxisValue(Dimension.DISTANCE, item);
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public final Number getZ(final int series, final int item)
374     {
375         return getZValue(series, item);
376     }
377 
378     /** {@inheritDoc} */
379     @Override
380     public final Comparable<String> getSeriesKey(final int series)
381     {
382         return getCaption();
383     }
384 
385     /** {@inheritDoc} */
386     @SuppressWarnings("rawtypes")
387     @Override
388     public final int indexOf(final Comparable seriesKey)
389     {
390         return 0;
391     }
392 
393     /** {@inheritDoc} */
394     @Override
395     public final DomainOrder getDomainOrder()
396     {
397         return DomainOrder.ASCENDING;
398     }
399 
400     /** {@inheritDoc} */
401     @Override
402     public final double getZValue(final int series, final int item)
403     {
404         // default 1 series
405         return getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME));
406     }
407 
408     /** {@inheritDoc} */
409     @Override
410     public final int getSeriesCount()
411     {
412         return 1; // default
413     }
414 
415     /** {@inheritDoc} */
416     @Override
417     public int getRangeBinCount()
418     {
419         return this.dataPool.getBinCount(Dimension.DISTANCE);
420     }
421 
422     /**
423      * Returns the status label when the mouse is over the given location.
424      * @param domainValue double; domain value (x-axis)
425      * @param rangeValue double; range value (y-axis)
426      * @return String; status label when the mouse is over the given location
427      */
428     protected final String getStatusLabel(final double domainValue, final double rangeValue)
429     {
430         if (this.dataPool == null)
431         {
432             return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
433         }
434         int i = this.dataPool.getAxisBin(Dimension.DISTANCE, rangeValue);
435         int j = this.dataPool.getAxisBin(Dimension.TIME, domainValue);
436         int item = j * this.dataPool.getBinCount(Dimension.DISTANCE) + i;
437         double zValue = scale(
438                 getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME)));
439         return String.format("time %.0fs, distance %.0fm, " + this.valueFormat, domainValue, rangeValue, zValue);
440     }
441 
442     /** {@inheritDoc} */
443     @Override
444     protected final void increaseTime(final Time time)
445     {
446         if (this.dataPool != null) // dataPool is null at construction
447         {
448             this.dataPool.increaseTime(time);
449         }
450     }
451 
452     /**
453      * Obtain value for cell from the data pool.
454      * @param item int; item number
455      * @param cellLength double; cell length
456      * @param cellSpan double; cell duration
457      * @return double; value for cell from the data pool
458      */
459     protected abstract double getValue(int item, double cellLength, double cellSpan);
460 
461     /**
462      * Scale the value from SI to the desired unit for users.
463      * @param si double; SI value
464      * @return double; scaled value
465      */
466     protected abstract double scale(double si);
467 
468     /**
469      * Returns the contour data type for use in a {@code ContourDataSource}.
470      * @return CountorDataType; contour data type
471      */
472     protected abstract ContourDataType<Z, ?> getContourDataType();
473 
474 }