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