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.util.ArrayList;
7
8 import javax.swing.ButtonGroup;
9 import javax.swing.JFrame;
10 import javax.swing.JLabel;
11 import javax.swing.JMenu;
12 import javax.swing.JPopupMenu;
13 import javax.swing.JRadioButtonMenuItem;
14 import javax.swing.SwingConstants;
15 import javax.swing.event.EventListenerList;
16
17 import org.djunits.unit.FrequencyUnit;
18 import org.djunits.unit.LinearDensityUnit;
19 import org.djunits.unit.SpeedUnit;
20 import org.djunits.value.vdouble.scalar.Frequency;
21 import org.djunits.value.vdouble.scalar.Length;
22 import org.djunits.value.vdouble.scalar.LinearDensity;
23 import org.djunits.value.vdouble.scalar.Speed;
24 import org.djunits.value.vdouble.scalar.Time;
25 import org.jfree.chart.ChartFactory;
26 import org.jfree.chart.ChartPanel;
27 import org.jfree.chart.JFreeChart;
28 import org.jfree.chart.StandardChartTheme;
29 import org.jfree.chart.axis.NumberAxis;
30 import org.jfree.chart.axis.ValueAxis;
31 import org.jfree.chart.event.AxisChangeEvent;
32 import org.jfree.chart.labels.XYItemLabelGenerator;
33 import org.jfree.chart.plot.PlotOrientation;
34 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
35 import org.jfree.data.DomainOrder;
36 import org.jfree.data.general.DatasetChangeEvent;
37 import org.jfree.data.general.DatasetChangeListener;
38 import org.jfree.data.general.DatasetGroup;
39 import org.jfree.data.xy.XYDataset;
40 import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
41 import org.opentrafficsim.core.gtu.GTUException;
42 import org.opentrafficsim.core.gtu.GTUType;
43 import org.opentrafficsim.core.gtu.RelativePosition;
44 import org.opentrafficsim.core.network.NetworkException;
45 import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
46 import org.opentrafficsim.road.network.lane.AbstractSensor;
47 import org.opentrafficsim.road.network.lane.Lane;
48
49
50
51
52
53
54
55
56
57
58
59
60 public class FundamentalDiagram extends JFrame implements XYDataset, ActionListener
61 {
62
63 private static final long serialVersionUID = 20140701L;
64
65
66 private JFreeChart chartPanel;
67
68
69 private final String caption;
70
71
72 private final Length.Rel position;
73
74
75 private final JLabel statusLabel;
76
77
78 private final Time.Rel aggregationTime;
79
80
81
82
83 public final Time.Rel getAggregationTime()
84 {
85 return this.aggregationTime;
86 }
87
88
89 private ArrayList<Sample> samples = new ArrayList<Sample>();
90
91
92 private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER), new LinearDensity(200,
93 LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density", "density %.1f veh/km");
94
95
96
97
98 public final Axis getDensityAxis()
99 {
100 return this.densityAxis;
101 }
102
103
104 private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(180, SpeedUnit.KM_PER_HOUR), null,
105 0d, "Speed [km/h]", "Speed", "speed %.0f km/h");
106
107
108
109
110 public final Axis getSpeedAxis()
111 {
112 return this.speedAxis;
113 }
114
115
116
117
118 public final Axis getFlowAxis()
119 {
120 return this.flowAxis;
121 }
122
123
124 private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR),
125 new Frequency(3000d, FrequencyUnit.HERTZ), null, 0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h");
126
127
128 private Axis xAxis;
129
130
131 private Axis yAxis;
132
133
134 private transient EventListenerList listenerList = new EventListenerList();
135
136
137 private DatasetGroup datasetGroup = null;
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 public FundamentalDiagram(final String caption, final Time.Rel aggregationTime, final Lane lane,
167 final Length.Rel position) throws NetworkException
168 {
169 if (aggregationTime.getSI() <= 0)
170 {
171 throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
172 }
173 this.aggregationTime = aggregationTime;
174 this.caption = caption;
175 this.position = position;
176 ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
177 this.chartPanel =
178 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
179 FixCaption.fixCaption(this.chartPanel);
180 final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
181 renderer.setBaseLinesVisible(true);
182 renderer.setBaseShapesVisible(true);
183 renderer.setBaseItemLabelGenerator(new XYItemLabelGenerator()
184 {
185 @Override
186 public String generateLabel(final XYDataset dataset, final int series, final int item)
187 {
188 return String.format("%.0fs", item * aggregationTime.getSI());
189 }
190 });
191 renderer.setBaseItemLabelsVisible(true);
192 final ChartPanel cp = new ChartPanel(this.chartPanel);
193 PointerHandler ph = new PointerHandler()
194 {
195
196 @Override
197 void updateHint(final double domainValue, final double rangeValue)
198 {
199 if (Double.isNaN(domainValue))
200 {
201 setStatusText(" ");
202 return;
203 }
204 String s1 = String.format(getXAxisFormat(), domainValue);
205 String s2 = String.format(getYAxisFormat(), rangeValue);
206 setStatusText(s1 + ", " + s2);
207 }
208
209 };
210 cp.addMouseMotionListener(ph);
211 cp.addMouseListener(ph);
212 cp.setMouseWheelEnabled(true);
213 final JMenu subMenu = new JMenu("Set layout");
214 final ButtonGroup group = new ButtonGroup();
215 final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
216 addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
217 addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
218 actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
219 final JPopupMenu popupMenu = cp.getPopupMenu();
220 popupMenu.insert(subMenu, 0);
221 this.add(cp, BorderLayout.CENTER);
222 this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
223 this.add(this.statusLabel, BorderLayout.SOUTH);
224 new FundamentalDiagramSensor(lane, position, null);
225 }
226
227
228
229
230
231 public final void setStatusText(final String newText)
232 {
233 this.statusLabel.setText(newText);
234 }
235
236
237
238
239
240 public final Length.Rel getPosition()
241 {
242 return this.position;
243 }
244
245
246
247
248
249
250
251
252
253
254
255 private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
256 final Axis yAxisToSelect, final boolean selected)
257 {
258 final JRadioButtonMenuItem item =
259 new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
260 item.setSelected(selected);
261 item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
262 item.addActionListener(this);
263 subMenu.add(item);
264 group.add(item);
265 return item;
266 }
267
268
269
270
271
272
273 public final void addData(final LaneBasedGTU gtu) throws GTUException
274 {
275 Time.Abs detectionTime = gtu.getSimulator().getSimulatorTime().getTime();
276
277 final int timeBin = (int) Math.floor(detectionTime.getSI() / this.aggregationTime.getSI());
278
279 while (timeBin >= this.samples.size())
280 {
281 this.samples.add(new Sample());
282 }
283 Sample sample = this.samples.get(timeBin);
284 sample.addData(gtu.getVelocity(detectionTime));
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
408
409
410
411 return result;
412 }
413
414
415 @Override
416 public final Number getX(final int series, final int item)
417 {
418 return getXValue(series, item);
419 }
420
421
422 @Override
423 public final double getXValue(final int series, final int item)
424 {
425 return getSample(item, this.xAxis);
426 }
427
428
429 @Override
430 public final Number getY(final int series, final int item)
431 {
432 return getYValue(series, item);
433 }
434
435
436 @Override
437 public final double getYValue(final int series, final int item)
438 {
439 return getSample(item, this.yAxis);
440 }
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470 class Sample
471 {
472
473 private double harmonicMeanSpeed;
474
475
476 private double flow;
477
478
479
480
481
482
483 public double getValue(final Axis axis)
484 {
485 if (axis == getDensityAxis())
486 {
487 return this.flow * 3600 / getAggregationTime().getSI() / this.harmonicMeanSpeed;
488 }
489 else if (axis == getFlowAxis())
490 {
491 return this.flow * 3600 / getAggregationTime().getSI();
492 }
493 else if (axis == getSpeedAxis())
494 {
495 return this.harmonicMeanSpeed * 3600 / 1000;
496 }
497 else
498 {
499 throw new Error("Sample.getValue: Can not identify axis");
500 }
501 }
502
503
504
505
506
507 public void addData(final Speed speed)
508 {
509 double sumReciprocalSpeeds = 0;
510 if (this.flow > 0)
511 {
512 sumReciprocalSpeeds = this.flow / this.harmonicMeanSpeed;
513 }
514 this.flow += 1;
515 sumReciprocalSpeeds += 1d / speed.getSI();
516 this.harmonicMeanSpeed = this.flow / sumReciprocalSpeeds;
517 }
518 }
519
520
521 @Override
522 public final void actionPerformed(final ActionEvent actionEvent)
523 {
524 final String command = actionEvent.getActionCommand();
525
526 final String[] fields = command.split("[/]");
527 if (fields.length == 2)
528 {
529 for (String field : fields)
530 {
531 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
532 {
533 if (field == fields[0])
534 {
535 this.yAxis = this.densityAxis;
536 }
537 else
538 {
539 this.xAxis = this.densityAxis;
540 }
541 }
542 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
543 {
544 if (field == fields[0])
545 {
546 this.yAxis = this.flowAxis;
547 }
548 else
549 {
550 this.xAxis = this.flowAxis;
551 }
552 }
553 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
554 {
555 if (field == fields[0])
556 {
557 this.yAxis = this.speedAxis;
558 }
559 else
560 {
561 this.xAxis = this.speedAxis;
562 }
563 }
564 else
565 {
566 throw new Error("Cannot find axis name: " + field);
567 }
568 }
569 reGraph();
570 }
571 else
572 {
573 throw new Error("Unknown ActionEvent");
574 }
575 }
576
577
578
579
580
581
582
583
584
585
586
587 class FundamentalDiagramSensor extends AbstractSensor
588 {
589
590 private static final long serialVersionUID = 20150203L;
591
592
593
594
595
596
597
598
599 public FundamentalDiagramSensor(final Lane lane, final Length.Rel longitudinalPosition,
600 final OTSDEVSSimulatorInterface simulator) throws NetworkException
601 {
602 super(lane, longitudinalPosition, RelativePosition.REFERENCE, "FUNDAMENTAL_DIAGRAM_SENSOR@"
603 + lane.toString(), simulator);
604 lane.addSensor(this, GTUType.ALL);
605 }
606
607
608 @Override
609 public void trigger(final LaneBasedGTU gtu)
610 {
611 try
612 {
613 addData(gtu);
614 }
615 catch (GTUException exception)
616 {
617 exception.printStackTrace();
618 }
619 }
620
621
622 public final String toString()
623 {
624 return "FundamentalDiagramSensor at " + getLongitudinalPosition();
625 }
626
627 }
628
629 }