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