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