View Javadoc
1   /**
2    *
3    */
4   package org.opentrafficsim.swing.graphs;
5   
6   import java.awt.BasicStroke;
7   import java.awt.Color;
8   import java.awt.Font;
9   import java.awt.event.ActionEvent;
10  import java.awt.event.ActionListener;
11  import java.util.List;
12  import java.util.Optional;
13  
14  import javax.swing.ButtonGroup;
15  import javax.swing.JMenu;
16  import javax.swing.JPopupMenu;
17  import javax.swing.JRadioButtonMenuItem;
18  
19  import org.djunits.value.vdouble.scalar.Duration;
20  import org.jfree.chart.ChartMouseEvent;
21  import org.jfree.chart.ChartMouseListener;
22  import org.jfree.chart.annotations.XYAnnotation;
23  import org.jfree.chart.annotations.XYLineAnnotation;
24  import org.jfree.chart.annotations.XYTextAnnotation;
25  import org.jfree.chart.entity.AxisEntity;
26  import org.jfree.chart.entity.XYItemEntity;
27  import org.jfree.chart.ui.TextAnchor;
28  import org.jfree.data.Range;
29  import org.opentrafficsim.draw.graphs.FundamentalDiagram;
30  import org.opentrafficsim.draw.graphs.FundamentalDiagram.Quantity;
31  import org.opentrafficsim.draw.graphs.GraphUtil;
32  
33  /**
34   * Embed a FundamentalDiagram in a Swing JPanel.
35   * <p>
36   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
37   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
38   * </p>
39   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
40   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
41   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
42   */
43  public class SwingFundamentalDiagram extends SwingPlot
44  {
45      /** */
46      private static final long serialVersionUID = 20190823L;
47  
48      /**
49       * Construct a new Swing container for FundamentalDiagram plot.
50       * @param plot the plot to embed
51       */
52      public SwingFundamentalDiagram(final FundamentalDiagram plot)
53      {
54          super(plot);
55      }
56  
57      @Override
58      protected Optional<ChartMouseListener> getChartMouseListener()
59      {
60          ChartMouseListener toggle = !getPlot().hasLineFD() && getPlot().getSource().getNumberOfSeries() < 2 ? null
61                  : GraphUtil.getToggleSeriesByLegendListener(getPlot().getLegend(), getPlot().getLaneVisible());
62          return Optional.of(new ChartMouseListener()
63          {
64              @SuppressWarnings("unchecked")
65              @Override
66              public void chartMouseClicked(final ChartMouseEvent event)
67              {
68                  if (toggle != null)
69                  {
70                      toggle.chartMouseClicked(event); // forward as we use two listeners
71                  }
72                  // remove any line annotations
73                  for (XYAnnotation annotation : ((List<XYAnnotation>) getPlot().getChart().getXYPlot().getAnnotations()))
74                  {
75                      if (annotation instanceof XYLineAnnotation)
76                      {
77                          getPlot().getChart().getXYPlot().removeAnnotation(annotation);
78                      }
79                  }
80                  // add line annotation for each item in series if the user clicked in an item
81                  if (event.getEntity() instanceof XYItemEntity)
82                  {
83                      XYItemEntity itemEntity = (XYItemEntity) event.getEntity();
84                      int series = itemEntity.getSeriesIndex();
85                      for (int i = 0; i < getPlot().getItemCount(series) - 1; i++)
86                      {
87                          XYLineAnnotation annotation = new XYLineAnnotation(getPlot().getXValue(series, i),
88                                  getPlot().getYValue(series, i), getPlot().getXValue(series, i + 1),
89                                  getPlot().getYValue(series, i + 1), new BasicStroke(1.0f), Color.WHITE);
90                          getPlot().getChart().getXYPlot().addAnnotation(annotation);
91                      }
92                  }
93                  else if (event.getEntity() instanceof AxisEntity)
94                  {
95                      if (((AxisEntity) event.getEntity()).getAxis().equals(getPlot().getChart().getXYPlot().getDomainAxis()))
96                      {
97                          Quantity old = getPlot().getDomainQuantity();
98                          getPlot().setDomainQuantity(getPlot().getOtherQuantity());
99                          getPlot().setOtherQuantity(old);
100                         getPlot().getChart().getXYPlot().getDomainAxis().setLabel(getPlot().getDomainQuantity().label());
101                         getPlot().getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
102                     }
103                     else
104                     {
105                         Quantity old = getPlot().getRangeQuantity();
106                         getPlot().setRangeQuantity(getPlot().getOtherQuantity());
107                         getPlot().setOtherQuantity(old);
108                         getPlot().getChart().getXYPlot().getRangeAxis().setLabel(getPlot().getRangeQuantity().label());
109                         getPlot().getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
110                     }
111                 }
112             }
113 
114             @SuppressWarnings("unchecked")
115             @Override
116             public void chartMouseMoved(final ChartMouseEvent event)
117             {
118                 if (toggle != null)
119                 {
120                     toggle.chartMouseMoved(event); // forward as we use two listeners
121                 }
122                 boolean clearText = true;
123                 // set text annotation and status text to time of item
124                 if (event.getEntity() instanceof XYItemEntity)
125                 {
126                     // create time info for status label
127                     XYItemEntity itemEntity = (XYItemEntity) event.getEntity();
128                     int series = itemEntity.getSeriesIndex();
129                     if (!getPlot().hasLineFD() || series != getPlot().getSeriesCount() - 1)
130                     {
131                         clearText = false;
132                         int item = itemEntity.getItem();
133                         double t = item * getPlot().getSource().getUpdateInterval().si;
134                         getPlot().setTimeInfo(String.format(", %.0fs", t));
135                         double x = getPlot().getXValue(series, item);
136                         double y = getPlot().getYValue(series, item);
137                         Range domain = getPlot().getChart().getXYPlot().getDomainAxis().getRange();
138                         Range range = getPlot().getChart().getXYPlot().getRangeAxis().getRange();
139                         TextAnchor anchor;
140                         if (range.getUpperBound() - y < y - range.getLowerBound())
141                         {
142                             // upper half
143                             if (domain.getUpperBound() - x < x - domain.getLowerBound())
144                             {
145                                 // upper right quadrant
146                                 anchor = TextAnchor.TOP_RIGHT;
147                             }
148                             else
149                             {
150                                 // upper left quadrant, can't use TOP_LEFT as text will be under mouse pointer
151                                 if ((range.getUpperBound() - y)
152                                         / (range.getUpperBound() - range.getLowerBound()) < (x - domain.getLowerBound())
153                                                 / (domain.getUpperBound() - domain.getLowerBound()))
154                                 {
155                                     // closer to top (at least relatively) so move text down
156                                     anchor = TextAnchor.TOP_RIGHT;
157                                 }
158                                 else
159                                 {
160                                     // closer to left (at least relatively) so move text right
161                                     anchor = TextAnchor.BOTTOM_LEFT;
162                                 }
163                             }
164                         }
165                         else if (domain.getUpperBound() - x < x - domain.getLowerBound())
166                         {
167                             // lower right quadrant
168                             anchor = TextAnchor.BOTTOM_RIGHT;
169                         }
170                         else
171                         {
172                             // lower left quadrant
173                             anchor = TextAnchor.BOTTOM_LEFT;
174                         }
175                         XYTextAnnotation textAnnotation = new XYTextAnnotation(String.format("%.0fs", t), x, y);
176                         textAnnotation.setTextAnchor(anchor);
177                         textAnnotation.setFont(textAnnotation.getFont().deriveFont(14.0f).deriveFont(Font.BOLD));
178                         getPlot().getChart().getXYPlot().addAnnotation(textAnnotation);
179                     }
180                 }
181                 // remove texts when mouse is elsewhere, or on FD line
182                 if (clearText)
183                 {
184                     for (XYAnnotation annotation : ((List<XYAnnotation>) getPlot().getChart().getXYPlot().getAnnotations()))
185                     {
186                         if (annotation instanceof XYTextAnnotation)
187                         {
188                             getPlot().getChart().getXYPlot().removeAnnotation(annotation);
189                         }
190                     }
191                     getPlot().setTimeInfo("");
192                 }
193             }
194         });
195     }
196 
197     @Override
198     protected void addPopUpMenuItems(final JPopupMenu popupMenu)
199     {
200         super.addPopUpMenuItems(popupMenu);
201         popupMenu.insert(new JPopupMenu.Separator(), 0);
202 
203         JMenu updMenu = new JMenu("Update frequency");
204         ButtonGroup updGroup = new ButtonGroup();
205         for (int f : getPlot().getSource().getPossibleUpdateFrequencies())
206         {
207             String format = "%dx";
208             JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, f));
209             item.setSelected(f == 1);
210             item.addActionListener(new ActionListener()
211             {
212                 @Override
213                 public void actionPerformed(final ActionEvent e)
214                 {
215 
216                     if ((int) (.5 + getPlot().getSource().getAggregationPeriod().si
217                             / getPlot().getSource().getUpdateInterval().si) != f)
218                     {
219                         Duration interval = Duration.ofSI(getPlot().getSource().getAggregationPeriod().si / f);
220                         for (FundamentalDiagram diagram : getPlot().getSource().getDiagrams())
221                         {
222                             diagram.setUpdateInterval(interval);
223                         }
224                         // the above setUpdateInterval also recalculates the virtual last update time
225                         // add half an interval to avoid any rounding issues
226                         getPlot().getSource().setUpdateInterval(interval, getPlot().getUpdateTime().plus(interval.times(0.5)));
227 
228                         for (FundamentalDiagram diagram : getPlot().getSource().getDiagrams())
229                         {
230                             diagram.getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
231                             diagram.getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
232                             diagram.notifyPlotChange();
233                         }
234                     }
235                 }
236             });
237             updGroup.add(item);
238             updMenu.add(item);
239         }
240         popupMenu.insert(updMenu, 0);
241 
242         JMenu aggMenu = new JMenu("Aggregation period");
243         ButtonGroup aggGroup = new ButtonGroup();
244         for (double t : getPlot().getSource().getPossibleAggregationPeriods())
245         {
246             double t2 = t;
247             String format = "%.0f s";
248             if (t >= 60.0)
249             {
250                 t2 = t / 60.0;
251                 format = "%.0f min";
252             }
253             JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, t2));
254             item.setSelected(t == getPlot().getSource().getAggregationPeriod().si);
255             item.addActionListener(new ActionListener()
256             {
257 
258                 @Override
259                 public void actionPerformed(final ActionEvent e)
260                 {
261                     if (getPlot().getSource().getAggregationPeriod().si != t)
262                     {
263                         int n = (int) (0.5 + getPlot().getSource().getAggregationPeriod().si
264                                 / getPlot().getSource().getUpdateInterval().si);
265                         Duration period = Duration.ofSI(t);
266                         Duration interval = period.divide(n);
267                         for (FundamentalDiagram diagram : getPlot().getSource().getDiagrams())
268                         {
269                             diagram.setUpdateInterval(interval);
270                         }
271                         // add half an interval to avoid any rounding issues
272                         getPlot().getSource().setAggregationPeriod(period);
273                         getPlot().getSource().setUpdateInterval(period.divide(n),
274                                 getPlot().getUpdateTime().plus(period.divide(n).times(0.5)));
275                         for (FundamentalDiagram diagram : getPlot().getSource().getDiagrams())
276                         {
277                             diagram.getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
278                             diagram.getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
279                             diagram.notifyPlotChange();
280                         }
281                     }
282                 }
283 
284             });
285             aggGroup.add(item);
286             aggMenu.add(item);
287         }
288         popupMenu.insert(aggMenu, 0);
289     }
290 
291     /**
292      * Retrieve the plot.
293      * @return the plot
294      */
295     @Override
296     public FundamentalDiagram getPlot()
297     {
298         return (FundamentalDiagram) super.getPlot();
299     }
300 
301 }