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