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
38
39
40
41
42
43
44
45
46 public class SwingFundamentalDiagram extends SwingPlot implements EventListener
47 {
48
49 private static final long serialVersionUID = 20190823L;
50
51
52 private boolean ignoreEvent = false;
53
54
55 private Map<JRadioButtonMenuItem, Double> aggregationPeriodButtons;
56
57
58 private Map<JRadioButtonMenuItem, Integer> updatesPerPeriodButtons;
59
60
61
62
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);
84 }
85
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
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);
139 }
140 boolean clearText = true;
141
142 if (event.getEntity() instanceof XYItemEntity)
143 {
144
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
161 if (domain.getUpperBound() - x < x - domain.getLowerBound())
162 {
163
164 anchor = TextAnchor.TOP_RIGHT;
165 }
166 else
167 {
168
169 if ((range.getUpperBound() - y)
170 / (range.getUpperBound() - range.getLowerBound()) < (x - domain.getLowerBound())
171 / (domain.getUpperBound() - domain.getLowerBound()))
172 {
173
174 anchor = TextAnchor.TOP_RIGHT;
175 }
176 else
177 {
178
179 anchor = TextAnchor.BOTTOM_LEFT;
180 }
181 }
182 }
183 else if (domain.getUpperBound() - x < x - domain.getLowerBound())
184 {
185
186 anchor = TextAnchor.BOTTOM_RIGHT;
187 }
188 else
189 {
190
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
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
282
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 }