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