1 package org.opentrafficsim.graphs;
2
3 import java.awt.BorderLayout;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6 import java.io.Serializable;
7 import java.util.ArrayList;
8
9 import javax.swing.ButtonGroup;
10 import javax.swing.JFrame;
11 import javax.swing.JLabel;
12 import javax.swing.JMenu;
13 import javax.swing.JPopupMenu;
14 import javax.swing.JRadioButtonMenuItem;
15 import javax.swing.SwingConstants;
16 import javax.swing.event.EventListenerList;
17
18 import nl.tudelft.simulation.dsol.SimRuntimeException;
19
20 import org.djunits.unit.FrequencyUnit;
21 import org.djunits.unit.LinearDensityUnit;
22 import org.djunits.unit.SpeedUnit;
23 import org.djunits.value.vdouble.scalar.Duration;
24 import org.djunits.value.vdouble.scalar.Frequency;
25 import org.djunits.value.vdouble.scalar.LinearDensity;
26 import org.djunits.value.vdouble.scalar.Speed;
27 import org.jfree.chart.ChartFactory;
28 import org.jfree.chart.ChartPanel;
29 import org.jfree.chart.JFreeChart;
30 import org.jfree.chart.StandardChartTheme;
31 import org.jfree.chart.axis.NumberAxis;
32 import org.jfree.chart.axis.ValueAxis;
33 import org.jfree.chart.event.AxisChangeEvent;
34 import org.jfree.chart.plot.PlotOrientation;
35 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
36 import org.jfree.data.DomainOrder;
37 import org.jfree.data.general.DatasetChangeEvent;
38 import org.jfree.data.general.DatasetChangeListener;
39 import org.jfree.data.general.DatasetGroup;
40 import org.jfree.data.xy.XYDataset;
41 import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
42 import org.opentrafficsim.core.gtu.RelativePosition;
43 import org.opentrafficsim.core.network.NetworkException;
44 import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
45 import org.opentrafficsim.road.network.lane.AbstractSensor;
46 import org.opentrafficsim.road.network.lane.Lane;
47
48 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49
50
51
52
53
54
55
56
57
58
59
60
61 public class FundamentalDiagramLane extends JFrame implements XYDataset, ActionListener
62 {
63
64 private static final long serialVersionUID = 20140701L;
65
66
67 private JFreeChart chartPanel;
68
69
70 private final String caption;
71
72
73 private final JLabel statusLabel;
74
75
76 private final Duration aggregationTime;
77
78
79 private ArrayList<Sample> samples = new ArrayList<Sample>();
80
81
82 private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER), new LinearDensity(200,
83 LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density", "density %.1f veh/km");
84
85
86
87
88 public final Axis getDensityAxis()
89 {
90 return this.densityAxis;
91 }
92
93
94 private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(120, SpeedUnit.KM_PER_HOUR), null, 0d,
95 "Speed [km/h]", "Speed", "speed %.0f km/h");
96
97
98
99
100 public final Axis getSpeedAxis()
101 {
102 return this.speedAxis;
103 }
104
105
106
107
108 public final Axis getFlowAxis()
109 {
110 return this.flowAxis;
111 }
112
113
114 private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR), new Frequency(3000d, FrequencyUnit.HERTZ), null,
115 0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h");
116
117
118 private Axis xAxis;
119
120
121 private Axis yAxis;
122
123
124 private transient EventListenerList listenerList = new EventListenerList();
125
126
127 private DatasetGroup datasetGroup = null;
128
129
130 private final Lane lane;
131
132
133 private final OTSDEVSSimulatorInterface simulator;
134
135
136
137 int flow = 0;
138
139
140
141
142
143 public final String getYAxisFormat()
144 {
145 return this.yAxis.getFormat();
146 }
147
148
149
150
151
152 public final String getXAxisFormat()
153 {
154 return this.xAxis.getFormat();
155 }
156
157
158
159
160
161
162
163
164
165
166
167 public FundamentalDiagramLane(final String caption, final Duration aggregationTime, final Lane lane,
168 final OTSDEVSSimulatorInterface simulator) throws NetworkException, SimRuntimeException
169 {
170 if (aggregationTime.getSI() <= 0)
171 {
172 throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
173 }
174 this.aggregationTime = aggregationTime;
175 this.caption = caption;
176 this.lane = lane;
177 this.simulator = simulator;
178 ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
179 this.chartPanel =
180 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
181 FixCaption.fixCaption(this.chartPanel);
182 final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
183 renderer.setBaseShapesVisible(true);
184
185 final ChartPanel cp = new ChartPanel(this.chartPanel);
186 PointerHandler ph = new PointerHandler()
187 {
188
189 private static final long serialVersionUID = 120140000;
190
191
192 @Override
193 void updateHint(final double domainValue, final double rangeValue)
194 {
195 if (Double.isNaN(domainValue))
196 {
197 setStatusText(" ");
198 return;
199 }
200 String s1 = String.format(getXAxisFormat(), domainValue);
201 String s2 = String.format(getYAxisFormat(), rangeValue);
202 setStatusText(s1 + ", " + s2);
203 }
204
205 };
206 cp.addMouseMotionListener(ph);
207 cp.addMouseListener(ph);
208 cp.setMouseWheelEnabled(true);
209 final JMenu subMenu = new JMenu("Set layout");
210 final ButtonGroup group = new ButtonGroup();
211 final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
212 addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
213 addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
214 actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
215 final JPopupMenu popupMenu = cp.getPopupMenu();
216 popupMenu.insert(subMenu, 0);
217 this.add(cp, BorderLayout.CENTER);
218 this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
219 this.add(this.statusLabel, BorderLayout.SOUTH);
220 simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
221 new FlowSensor(lane);
222 }
223
224
225
226
227
228 public final void setStatusText(final String newText)
229 {
230 this.statusLabel.setText(newText);
231 }
232
233
234
235
236 public final Duration getAggregationTime()
237 {
238 return this.aggregationTime;
239 }
240
241
242
243
244
245
246
247
248
249
250
251 private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
252 final Axis yAxisToSelect, final boolean selected)
253 {
254 final JRadioButtonMenuItem item =
255 new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
256 item.setSelected(selected);
257 item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
258 item.addActionListener(this);
259 subMenu.add(item);
260 group.add(item);
261 return item;
262 }
263
264
265
266
267
268 public final void addData() throws SimRuntimeException
269 {
270
271 double n = this.lane.getGtuList().size();
272 double density = n / this.lane.getLength().si;
273 if (density > 0.0)
274 {
275 double meanSpeed = 0.0;
276 for (LaneBasedGTU gtu : this.lane.getGtuList())
277 {
278 meanSpeed += 1 / gtu.getSpeed().si;
279 }
280 meanSpeed = n / meanSpeed;
281 this.samples.add(new Sample(meanSpeed, density, this.flow / this.aggregationTime.si));
282 this.flow = 0;
283 }
284 this.simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
285 }
286
287
288
289
290
291
292 private static void configureAxis(final ValueAxis valueAxis, final Axis axis)
293 {
294 valueAxis.setLabel("\u2192 " + axis.getName());
295 valueAxis.setRange(axis.getMinimumValue().getInUnit(), axis.getMaximumValue().getInUnit());
296 }
297
298
299
300
301 public final void reGraph()
302 {
303 NumberAxis numberAxis = new NumberAxis();
304 configureAxis(numberAxis, this.xAxis);
305 this.chartPanel.getXYPlot().setDomainAxis(numberAxis);
306 this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
307 numberAxis = new NumberAxis();
308 configureAxis(numberAxis, this.yAxis);
309 this.chartPanel.getXYPlot().setRangeAxis(numberAxis);
310 this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
311 notifyListeners(new DatasetChangeEvent(this, null));
312 }
313
314
315
316
317
318 private void notifyListeners(final DatasetChangeEvent event)
319 {
320 for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
321 {
322 dcl.datasetChanged(event);
323 }
324 }
325
326
327 @Override
328 public final int getSeriesCount()
329 {
330 return 1;
331 }
332
333
334 @Override
335 public final Comparable<Integer> getSeriesKey(final int series)
336 {
337 return series;
338 }
339
340
341 @SuppressWarnings("rawtypes")
342 @Override
343 public final int indexOf(final Comparable seriesKey)
344 {
345 if (seriesKey instanceof Integer)
346 {
347 return (Integer) seriesKey;
348 }
349 return -1;
350 }
351
352
353 @Override
354 public final void addChangeListener(final DatasetChangeListener listener)
355 {
356 this.listenerList.add(DatasetChangeListener.class, listener);
357 }
358
359
360 @Override
361 public final void removeChangeListener(final DatasetChangeListener listener)
362 {
363 this.listenerList.remove(DatasetChangeListener.class, listener);
364 }
365
366
367 @Override
368 public final DatasetGroup getGroup()
369 {
370 return this.datasetGroup;
371 }
372
373
374 @Override
375 public final void setGroup(final DatasetGroup group)
376 {
377 this.datasetGroup = group;
378 }
379
380
381 @Override
382 public final DomainOrder getDomainOrder()
383 {
384 return DomainOrder.ASCENDING;
385 }
386
387
388 @Override
389 public final int getItemCount(final int series)
390 {
391 return this.samples.size();
392 }
393
394
395
396
397
398
399
400 private Double getSample(final int item, final Axis axis)
401 {
402 if (item >= this.samples.size())
403 {
404 return Double.NaN;
405 }
406 double result = this.samples.get(item).getValue(axis);
407 return result;
408 }
409
410
411 @Override
412 public final Number getX(final int series, final int item)
413 {
414 return getXValue(series, item);
415 }
416
417
418 @Override
419 public final double getXValue(final int series, final int item)
420 {
421 return getSample(item, this.xAxis);
422 }
423
424
425 @Override
426 public final Number getY(final int series, final int item)
427 {
428 return getYValue(series, item);
429 }
430
431
432 @Override
433 public final double getYValue(final int series, final int item)
434 {
435 return getSample(item, this.yAxis);
436 }
437
438
439 @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ")
440 @Override
441 public final void actionPerformed(final ActionEvent actionEvent)
442 {
443 final String command = actionEvent.getActionCommand();
444
445 final String[] fields = command.split("[/]");
446 if (fields.length == 2)
447 {
448 for (String field : fields)
449 {
450 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
451 {
452 if (field == fields[0])
453 {
454 this.yAxis = this.densityAxis;
455 }
456 else
457 {
458 this.xAxis = this.densityAxis;
459 }
460 }
461 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
462 {
463 if (field == fields[0])
464 {
465 this.yAxis = this.flowAxis;
466 }
467 else
468 {
469 this.xAxis = this.flowAxis;
470 }
471 }
472 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
473 {
474 if (field == fields[0])
475 {
476 this.yAxis = this.speedAxis;
477 }
478 else
479 {
480 this.xAxis = this.speedAxis;
481 }
482 }
483 else
484 {
485 throw new Error("Cannot find axis name: " + field);
486 }
487 }
488 reGraph();
489 }
490 else
491 {
492 throw new Error("Unknown ActionEvent");
493 }
494 }
495
496
497 @Override
498 public final String toString()
499 {
500 return "FundamentalDiagramLane [caption=" + this.caption + ", aggregationTime=" + this.aggregationTime
501 + ", samples.size=" + this.samples.size() + ", lane=" + this.lane + ", flow=" + this.flow + "]";
502 }
503
504
505
506
507
508
509
510 class Sample implements Serializable
511 {
512
513 private static final long serialVersionUID = 20140000L;
514
515
516 private final double meanSpeed;
517
518
519 private final double density;
520
521
522 private final double flow;
523
524
525
526
527
528
529 public Sample(final double meanSpeed, final double density, final double flow)
530 {
531 super();
532 this.meanSpeed = meanSpeed;
533 this.density = density;
534 this.flow = flow;
535 }
536
537
538
539
540
541
542 public double getValue(final Axis axis)
543 {
544 if (axis == getDensityAxis())
545 {
546 return 1000.0 * this.density;
547 }
548 else if (axis == getFlowAxis())
549 {
550 return 3600.0 * this.meanSpeed * this.density;
551 }
552 else if (axis == getSpeedAxis())
553 {
554 return 3.6 * this.meanSpeed;
555 }
556 else
557 {
558 throw new Error("Sample.getValue: Can not identify axis");
559 }
560 }
561
562
563 @Override
564 public final String toString()
565 {
566 return "Sample [meanSpeed=" + this.meanSpeed + ", density=" + this.density + ", flow=" + this.flow + "]";
567 }
568 }
569
570
571 private class FlowSensor extends AbstractSensor
572 {
573
574 private static final long serialVersionUID = 1L;
575
576
577
578
579 public FlowSensor(final Lane lane)
580 {
581 super(lane, lane.getLength().divideBy(2.0), RelativePosition.FRONT, "FLOW", null);
582 }
583
584
585 @Override
586 public void trigger(final LaneBasedGTU gtu)
587 {
588 FundamentalDiagramLane.this.flow += 1;
589 }
590
591
592 @Override
593 public final String toString()
594 {
595 return "FlowSensor []";
596 }
597
598 }
599
600 }