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