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