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