1 package org.opentrafficsim.animation.graphs;
2
3 import java.awt.Color;
4 import java.util.ArrayList;
5 import java.util.EnumSet;
6 import java.util.List;
7 import java.util.Set;
8
9 import org.djunits.value.vdouble.scalar.Duration;
10 import org.djutils.event.EventListener;
11 import org.djutils.event.EventType;
12 import org.djutils.exceptions.Throw;
13 import org.djutils.immutablecollections.ImmutableList;
14 import org.jfree.chart.JFreeChart;
15 import org.jfree.chart.LegendItem;
16 import org.jfree.chart.LegendItemCollection;
17 import org.jfree.chart.axis.NumberAxis;
18 import org.jfree.chart.plot.XYPlot;
19 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
20 import org.jfree.data.DomainOrder;
21 import org.jfree.data.xy.XYDataset;
22 import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
23 import org.opentrafficsim.animation.graphs.FundamentalDiagram.FdPaintState;
24
25
26
27
28
29
30
31
32
33
34
35 public class FundamentalDiagram extends AbstractBoundedPlot<FdPaintState> implements XYDataset
36 {
37
38
39 private final FdDataSource source;
40
41
42 private final FdLine fdLine;
43
44
45 private Quantity domainQuantity;
46
47
48 private Quantity rangeQuantity;
49
50
51 private Quantity otherQuantity;
52
53
54 private final List<String> seriesLabels = new ArrayList<>();
55
56
57 private String timeInfo = "";
58
59
60 private LegendItemCollection legend;
61
62
63 private final List<Boolean> laneVisible = new ArrayList<>();
64
65
66
67
68
69
70
71
72
73 public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
74 final FdDataSource source, final FdLine fdLine)
75 {
76 super(source.getPlotScheduler(), caption, source.getAggregationPeriod(), source.getDelay());
77 Throw.when(domainQuantity.equals(rangeQuantity), IllegalArgumentException.class,
78 "Domain and range quantity should not be equal.");
79 this.fdLine = fdLine;
80 this.setDomainQuantity(domainQuantity);
81 this.setRangeQuantity(rangeQuantity);
82 Set<Quantity> quantities = EnumSet.allOf(Quantity.class);
83 quantities.remove(domainQuantity);
84 quantities.remove(rangeQuantity);
85 this.setOtherQuantity(quantities.iterator().next());
86 this.source = source;
87 int d = 0;
88 if (fdLine != null)
89 {
90 d = 1;
91 this.seriesLabels.add(fdLine.getName());
92 this.laneVisible.add(true);
93 }
94 for (int series = 0; series < source.getNumberOfSeries(); series++)
95 {
96 this.seriesLabels.add(series + d, source.getName(series));
97 this.laneVisible.add(true);
98 }
99 setChart(createChart());
100 setLowerDomainBound(0.0);
101 setLowerRangeBound(0.0);
102
103
104 source.addPlot(this);
105 }
106
107
108
109
110
111 private JFreeChart createChart()
112 {
113 NumberAxis xAxis = new NumberAxis(this.getDomainQuantity().label());
114 NumberAxis yAxis = new NumberAxis(this.getRangeQuantity().label());
115 XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer()
116 {
117
118 private static final long serialVersionUID = 20181022L;
119
120 @Override
121 public boolean isSeriesVisible(final int series)
122 {
123 return FundamentalDiagram.this.laneVisible.get(series);
124 }
125 };
126 renderer.setDefaultLinesVisible(false);
127 if (hasLineFD())
128 {
129 int series = getNumberOfSeries();
130 renderer.setSeriesLinesVisible(series, true);
131 renderer.setSeriesPaint(series, Color.BLACK);
132 renderer.setSeriesShapesVisible(series, false);
133 }
134 XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
135 boolean showLegend = true;
136 if (!hasLineFD() && getNumberOfSeries() < 2)
137 {
138 plot.setFixedLegendItems(null);
139 showLegend = false;
140 }
141 else
142 {
143 this.legend = new LegendItemCollection();
144 for (int i = 0; i < getNumberOfSeries(); i++)
145 {
146 LegendItem li = new LegendItem(this.source.getName(i));
147 li.setSeriesKey(i);
148 li.setShape(renderer.lookupLegendShape(i));
149 li.setFillPaint(renderer.lookupSeriesPaint(i));
150 this.legend.add(li);
151 }
152 if (hasLineFD())
153 {
154 LegendItem li = new LegendItem(this.fdLine.getName());
155 li.setSeriesKey(-1);
156 this.legend.add(li);
157 }
158 plot.setFixedLegendItems(this.legend);
159 showLegend = true;
160 }
161 return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
162 }
163
164
165
166
167
168 public ImmutableList<Integer> getPossibleUpdatesPerPeriod()
169 {
170 return this.source.getUpdatesPerPeriodSetting().values();
171 }
172
173
174
175
176
177 public int getDefaultUpdatesPerPeriod()
178 {
179 return this.source.getUpdatesPerPeriodSetting().getDefaultValue();
180 }
181
182
183
184
185
186 public ImmutableList<Duration> getPossibleAggregationPeriods()
187 {
188 return this.source.getAggregationPeriodSetting().values();
189 }
190
191
192
193
194
195 public Duration getDefaultAggregationPeriod()
196 {
197 return this.source.getAggregationPeriodSetting().getDefaultValue();
198 }
199
200
201
202
203
204
205
206 public void addListener(final EventListener listener, final EventType eventType)
207 {
208 this.source.addListener(listener, eventType);
209 }
210
211
212
213
214
215 public int getNumberOfSeries()
216 {
217 return this.source.getNumberOfSeries();
218 }
219
220
221
222
223
224 public Duration getUpdateInterval()
225 {
226 return this.source.getUpdateInterval();
227 }
228
229
230
231
232
233 public void setUpdatesPerPeriod(final int n)
234 {
235 this.source.setUpdatesPerPeriod(n);
236 invalidate();
237 }
238
239
240
241
242
243 public void setAggregationPeriod(final Duration period)
244 {
245 this.source.setAggregationPeriod(period);
246 invalidate();
247 }
248
249 @Override
250 protected FdPaintState emptyPaintState()
251 {
252 return new FdPaintState(new FdSeries[0], Duration.ZERO);
253 }
254
255 @Override
256 protected void calculatePaintState(final Duration time)
257 {
258 this.source.calculatePaintStateSafe(time);
259 }
260
261 @Override
262 protected void setPaintState()
263 {
264 super.setPaintState();
265 getChart().getXYPlot().zoomDomainAxes(0, null, null);
266 getChart().getXYPlot().zoomRangeAxes(0, null, null);
267 }
268
269 @Override
270 public int getSeriesCount()
271 {
272 return getPaintState().getSeriesCount() + (hasLineFD() ? 1 : 0);
273 }
274
275 @Override
276 public Comparable<String> getSeriesKey(final int series)
277 {
278 return this.seriesLabels.get(series);
279 }
280
281 @SuppressWarnings("rawtypes")
282 @Override
283 public int indexOf(final Comparable seriesKey)
284 {
285 int index = this.seriesLabels.indexOf(seriesKey);
286 return index < 0 ? 0 : index;
287 }
288
289 @Override
290 public DomainOrder getDomainOrder()
291 {
292 return DomainOrder.NONE;
293 }
294
295 @Override
296 public int getItemCount(final int series)
297 {
298 if (hasLineFD() && series == getSeriesCount() - 1)
299 {
300 return this.fdLine.getValues(this.domainQuantity).length;
301 }
302 return getPaintState().getItemCount(series);
303 }
304
305 @Override
306 public Number getX(final int series, final int item)
307 {
308 return getXValue(series, item);
309 }
310
311 @Override
312 public double getXValue(final int series, final int item)
313 {
314 if (hasLineFD() && series == getSeriesCount() - 1)
315 {
316 return this.fdLine.getValues(this.domainQuantity)[item];
317 }
318 return getPaintState().getValue(getDomainQuantity(), series, item);
319 }
320
321 @Override
322 public Number getY(final int series, final int item)
323 {
324 return getYValue(series, item);
325 }
326
327 @Override
328 public double getYValue(final int series, final int item)
329 {
330 if (hasLineFD() && series == getSeriesCount() - 1)
331 {
332 return this.fdLine.getValues(this.rangeQuantity)[item];
333 }
334 return getPaintState().getValue(getRangeQuantity(), series, item);
335 }
336
337 @Override
338 public GraphType getGraphType()
339 {
340 return GraphType.FUNDAMENTAL_DIAGRAM;
341 }
342
343 @Override
344 public String getStatusLabel(final double domainValue, final double rangeValue)
345 {
346 return getDomainQuantity().format(domainValue) + ", " + getRangeQuantity().format(rangeValue) + ", "
347 + getOtherQuantity().format(getDomainQuantity().computeOther(getRangeQuantity(), domainValue, rangeValue))
348 + getTimeInfo();
349 }
350
351
352
353
354
355 public LegendItemCollection getLegend()
356 {
357 return this.legend;
358 }
359
360
361
362
363
364 public List<Boolean> getLaneVisible()
365 {
366 return this.laneVisible;
367 }
368
369
370
371
372
373 public Quantity getDomainQuantity()
374 {
375 return this.domainQuantity;
376 }
377
378
379
380
381
382 public void setDomainQuantity(final Quantity domainQuantity)
383 {
384 this.domainQuantity = domainQuantity;
385 }
386
387
388
389
390
391 public Quantity getOtherQuantity()
392 {
393 return this.otherQuantity;
394 }
395
396
397
398
399
400 public void setOtherQuantity(final Quantity otherQuantity)
401 {
402 this.otherQuantity = otherQuantity;
403 }
404
405
406
407
408
409 public Quantity getRangeQuantity()
410 {
411 return this.rangeQuantity;
412 }
413
414
415
416
417
418 public void setRangeQuantity(final Quantity rangeQuantity)
419 {
420 this.rangeQuantity = rangeQuantity;
421 }
422
423
424
425
426
427 public String getTimeInfo()
428 {
429 return this.timeInfo;
430 }
431
432
433
434
435
436 public void setTimeInfo(final String timeInfo)
437 {
438 this.timeInfo = timeInfo;
439 }
440
441
442
443
444
445 public boolean hasLineFD()
446 {
447 return this.fdLine != null;
448 }
449
450 @Override
451 public String toString()
452 {
453 return "FundamentalDiagram [domainQuantity=" + this.getDomainQuantity() + ", rangeQuantity=" + this.getRangeQuantity()
454 + "]";
455 }
456
457
458
459
460
461
462 public enum Quantity
463 {
464
465 DENSITY
466 {
467 @Override
468 public String label()
469 {
470 return "Density [veh/km] \u2192";
471 }
472
473 @Override
474 public String format(final double value)
475 {
476 return String.format("%.0f veh/km", value);
477 }
478
479 @Override
480 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
481 {
482
483 return pairing.equals(FLOW) ? pairedValue / thisValue : thisValue * pairedValue;
484 }
485 },
486
487
488 FLOW
489 {
490 @Override
491 public String label()
492 {
493 return "Flow [veh/h] \u2192";
494 }
495
496 @Override
497 public String format(final double value)
498 {
499 return String.format("%.0f veh/h", value);
500 }
501
502 @Override
503 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
504 {
505
506 return pairing.equals(DENSITY) ? thisValue * pairedValue : thisValue / pairedValue;
507 }
508 },
509
510
511 SPEED
512 {
513 @Override
514 public String label()
515 {
516 return "Speed [km/h] \u2192";
517 }
518
519 @Override
520 public String format(final double value)
521 {
522 return String.format("%.1f km/h", value);
523 }
524
525 @Override
526 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
527 {
528
529 return pairing.equals(DENSITY) ? thisValue * pairedValue : pairedValue / thisValue;
530 }
531 };
532
533
534
535
536
537 public abstract String label();
538
539
540
541
542
543
544 public abstract String format(double value);
545
546
547
548
549
550
551
552
553 public abstract double computeOther(Quantity pairing, double thisValue, double pairedValue);
554
555 }
556
557
558
559
560 public interface FdLine
561 {
562
563
564
565
566
567
568 double[] getValues(Quantity quantity);
569
570
571
572
573
574 String getName();
575
576 }
577
578
579
580
581
582
583
584 public record FdSeries(float[] q, float[] v, float[] k)
585 {
586
587
588
589
590
591
592
593 public double getValue(final Quantity quantity, final int item)
594 {
595 switch (quantity)
596 {
597 case DENSITY:
598 return k()[item];
599 case FLOW:
600 return q()[item];
601 case SPEED:
602 return v()[item];
603 default:
604 throw new IllegalArgumentException("Unknown quantity " + quantity);
605 }
606 }
607
608 }
609
610
611
612
613
614
615 public record FdPaintState(FdSeries[] fdSeries, Duration getAvailableTime) implements PaintState
616 {
617
618
619
620
621
622 public int getSeriesCount()
623 {
624 return this.fdSeries().length;
625 }
626
627
628
629
630
631
632 private int getItemCount(final int series)
633 {
634 return fdSeries()[series].k().length;
635 }
636
637
638
639
640
641
642
643
644 private double getValue(final Quantity quantity, final int series, final int item)
645 {
646 return fdSeries()[series].getValue(quantity, item);
647 }
648
649 }
650
651 }