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