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