1 package org.opentrafficsim.draw.graphs;
2
3 import java.awt.BasicStroke;
4 import java.awt.Color;
5 import java.awt.Font;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
8 import java.time.Period;
9 import java.util.ArrayList;
10 import java.util.EnumSet;
11 import java.util.Iterator;
12 import java.util.LinkedHashMap;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.SortedSet;
17 import java.util.TreeSet;
18
19 import javax.swing.ButtonGroup;
20 import javax.swing.JMenu;
21 import javax.swing.JPopupMenu;
22 import javax.swing.JRadioButtonMenuItem;
23
24 import org.djunits.value.vdouble.scalar.Duration;
25 import org.djunits.value.vdouble.scalar.Length;
26 import org.djunits.value.vdouble.scalar.Time;
27 import org.djutils.exceptions.Throw;
28 import org.jfree.chart.ChartMouseEvent;
29 import org.jfree.chart.ChartMouseListener;
30 import org.jfree.chart.JFreeChart;
31 import org.jfree.chart.LegendItem;
32 import org.jfree.chart.LegendItemCollection;
33 import org.jfree.chart.annotations.XYAnnotation;
34 import org.jfree.chart.annotations.XYLineAnnotation;
35 import org.jfree.chart.annotations.XYTextAnnotation;
36 import org.jfree.chart.axis.NumberAxis;
37 import org.jfree.chart.entity.AxisEntity;
38 import org.jfree.chart.entity.XYItemEntity;
39 import org.jfree.chart.plot.XYPlot;
40 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
41 import org.jfree.chart.ui.TextAnchor;
42 import org.jfree.data.DomainOrder;
43 import org.jfree.data.Range;
44 import org.jfree.data.xy.XYDataset;
45 import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
46 import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
47 import org.opentrafficsim.kpi.sampling.Sampler;
48 import org.opentrafficsim.kpi.sampling.SamplingException;
49 import org.opentrafficsim.kpi.sampling.SpaceTimeRegion;
50 import org.opentrafficsim.kpi.sampling.Trajectory;
51 import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView;
52 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
53
54
55
56
57
58
59
60
61
62
63
64
65 public class FundamentalDiagram extends AbstractBoundedPlot implements XYDataset
66 {
67
68
69 private static final long serialVersionUID = 20101016L;
70
71
72 public static final double[] DEFAULT_PERIODS = new double[] { 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 900.0 };
73
74
75 public static final int[] DEFAULT_UPDATE_FREQUENCIES = new int[] { 1, 2, 3, 5, 10 };
76
77
78 private final FdSource source;
79
80
81 private Quantity domainQuantity;
82
83
84 private Quantity rangeQuantity;
85
86
87 private Quantity otherQuantity;
88
89
90 private final List<String> seriesLabels = new ArrayList<>();
91
92
93 private final GraphUpdater<Time> graphUpdater;
94
95
96 private String timeInfo = "";
97
98
99 private LegendItemCollection legend;
100
101
102 private final List<Boolean> laneVisible = new ArrayList<>();
103
104
105
106
107
108
109
110
111
112 public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
113 final OTSSimulatorInterface simulator, final FdSource source)
114 {
115 super(caption, source.getUpdateInterval(), simulator, source.getDelay());
116 Throw.when(domainQuantity.equals(rangeQuantity), IllegalArgumentException.class,
117 "Domain and range quantity should not be equal.");
118 this.domainQuantity = domainQuantity;
119 this.rangeQuantity = rangeQuantity;
120 Set<Quantity> quantities = EnumSet.allOf(Quantity.class);
121 quantities.remove(domainQuantity);
122 quantities.remove(rangeQuantity);
123 this.otherQuantity = quantities.iterator().next();
124 this.source = source;
125 for (int series = 0; series < source.getNumberOfSeries(); series++)
126 {
127 this.seriesLabels.add(series, source.getName(series));
128 this.laneVisible.add(true);
129 }
130 setChart(createChart());
131 setLowerDomainBound(0.0);
132 setLowerRangeBound(0.0);
133
134
135 this.graphUpdater = new GraphUpdater<>("Fundamental diagram worker", Thread.currentThread(), (t) ->
136 {
137 if (this.source != null)
138 {
139 this.source.increaseTime(t);
140 notifyPlotChange();
141 }
142 });
143 }
144
145
146
147
148
149
150
151
152
153
154
155
156
157 @SuppressWarnings("parameternumber")
158 public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
159 final OTSSimulatorInterface simulator, final Sampler<?> sampler,
160 final GraphCrossSection<KpiLaneDirection> crossSection, final boolean aggregateLanes,
161 final Duration aggregationTime, final boolean harmonic)
162 {
163 this(caption, domainQuantity, rangeQuantity, simulator,
164 sourceFromSampler(sampler, crossSection, aggregateLanes, aggregationTime, harmonic));
165 }
166
167
168
169
170
171
172
173
174
175
176
177
178 @SuppressWarnings("parameternumber")
179 public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
180 final OTSSimulatorInterface simulator, final Sampler<?> sampler, final GraphPath<KpiLaneDirection> path,
181 final boolean aggregateLanes, final Duration aggregationTime)
182 {
183 this(caption, domainQuantity, rangeQuantity, simulator,
184 sourceFromSampler(sampler, path, aggregateLanes, aggregationTime));
185 }
186
187
188
189
190
191 private JFreeChart createChart()
192 {
193 NumberAxis xAxis = new NumberAxis(this.domainQuantity.label());
194 NumberAxis yAxis = new NumberAxis(this.rangeQuantity.label());
195 XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer()
196 {
197
198 private static final long serialVersionUID = 20181022L;
199
200
201 @SuppressWarnings("synthetic-access")
202 @Override
203 public boolean isSeriesVisible(final int series)
204 {
205 return FundamentalDiagram.this.laneVisible.get(series);
206 }
207
208 };
209 renderer.setDefaultLinesVisible(false);
210 XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
211 boolean showLegend = true;
212 if (this.source.getNumberOfSeries() < 2)
213 {
214 plot.setFixedLegendItems(null);
215 showLegend = false;
216 }
217 else
218 {
219 this.legend = new LegendItemCollection();
220 for (int i = 0; i < this.source.getNumberOfSeries(); i++)
221 {
222 LegendItem li = new LegendItem(this.source.getName(i));
223 li.setSeriesKey(i);
224 li.setShape(renderer.lookupLegendShape(i));
225 li.setFillPaint(renderer.lookupSeriesPaint(i));
226 this.legend.add(li);
227 }
228 plot.setFixedLegendItems(this.legend);
229 showLegend = true;
230 }
231 return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
232 }
233
234
235 @Override
236 protected ChartMouseListener getChartMouseListener()
237 {
238 ChartMouseListener toggle = this.source.getNumberOfSeries() < 2 ? null
239 : GraphUtil.getToggleSeriesByLegendListener(this.legend, this.laneVisible);
240 return new ChartMouseListener()
241 {
242
243 @SuppressWarnings({ "unchecked", "synthetic-access" })
244 @Override
245 public void chartMouseClicked(final ChartMouseEvent event)
246 {
247 if (toggle != null)
248 {
249 toggle.chartMouseClicked(event);
250 }
251
252 for (XYAnnotation annotation : ((List<XYAnnotation>) getChart().getXYPlot().getAnnotations()))
253 {
254 if (annotation instanceof XYLineAnnotation)
255 {
256 getChart().getXYPlot().removeAnnotation(annotation);
257 }
258 }
259
260 if (event.getEntity() instanceof XYItemEntity)
261 {
262 XYItemEntity itemEntity = (XYItemEntity) event.getEntity();
263 int series = itemEntity.getSeriesIndex();
264 for (int i = 0; i < getItemCount(series) - 1; i++)
265 {
266 XYLineAnnotation annotation = new XYLineAnnotation(getXValue(series, i), getYValue(series, i),
267 getXValue(series, i + 1), getYValue(series, i + 1), new BasicStroke(1.0f), Color.WHITE);
268 getChart().getXYPlot().addAnnotation(annotation);
269 }
270 }
271 else if (event.getEntity() instanceof AxisEntity)
272 {
273 if (((AxisEntity) event.getEntity()).getAxis().equals(getChart().getXYPlot().getDomainAxis()))
274 {
275 Quantity old = FundamentalDiagram.this.domainQuantity;
276 FundamentalDiagram.this.domainQuantity = FundamentalDiagram.this.otherQuantity;
277 FundamentalDiagram.this.otherQuantity = old;
278 getChart().getXYPlot().getDomainAxis().setLabel(FundamentalDiagram.this.domainQuantity.label());
279 getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
280 }
281 else
282 {
283 Quantity old = FundamentalDiagram.this.rangeQuantity;
284 FundamentalDiagram.this.rangeQuantity = FundamentalDiagram.this.otherQuantity;
285 FundamentalDiagram.this.otherQuantity = old;
286 getChart().getXYPlot().getRangeAxis().setLabel(FundamentalDiagram.this.rangeQuantity.label());
287 getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
288 }
289 }
290 }
291
292
293 @SuppressWarnings({ "synthetic-access", "unchecked" })
294 @Override
295 public void chartMouseMoved(final ChartMouseEvent event)
296 {
297 if (toggle != null)
298 {
299 toggle.chartMouseMoved(event);
300 }
301
302 if (event.getEntity() instanceof XYItemEntity)
303 {
304
305 XYItemEntity itemEntity = (XYItemEntity) event.getEntity();
306 int series = itemEntity.getSeriesIndex();
307 int item = itemEntity.getItem();
308 double t = item * FundamentalDiagram.this.source.getUpdateInterval().si;
309 FundamentalDiagram.this.timeInfo = String.format(", %.0fs", t);
310 double x = getXValue(series, item);
311 double y = getYValue(series, item);
312 Range domain = getChart().getXYPlot().getDomainAxis().getRange();
313 Range range = getChart().getXYPlot().getRangeAxis().getRange();
314 TextAnchor anchor;
315 if (range.getUpperBound() - y < y - range.getLowerBound())
316 {
317
318 if (domain.getUpperBound() - x < x - domain.getLowerBound())
319 {
320
321 anchor = TextAnchor.TOP_RIGHT;
322 }
323 else
324 {
325
326 if ((range.getUpperBound() - y)
327 / (range.getUpperBound() - range.getLowerBound()) < (x - domain.getLowerBound())
328 / (domain.getUpperBound() - domain.getLowerBound()))
329 {
330
331 anchor = TextAnchor.TOP_RIGHT;
332 }
333 else
334 {
335
336 anchor = TextAnchor.BOTTOM_LEFT;
337 }
338 }
339 }
340 else if (domain.getUpperBound() - x < x - domain.getLowerBound())
341 {
342
343 anchor = TextAnchor.BOTTOM_RIGHT;
344 }
345 else
346 {
347
348 anchor = TextAnchor.BOTTOM_LEFT;
349 }
350 XYTextAnnotation textAnnotation = new XYTextAnnotation(String.format("%.0fs", t), x, y);
351 textAnnotation.setTextAnchor(anchor);
352 textAnnotation.setFont(textAnnotation.getFont().deriveFont(14.0f).deriveFont(Font.BOLD));
353 getChart().getXYPlot().addAnnotation(textAnnotation);
354 }
355
356 else
357 {
358 for (XYAnnotation annotation : ((List<XYAnnotation>) getChart().getXYPlot().getAnnotations()))
359 {
360 if (annotation instanceof XYTextAnnotation)
361 {
362 getChart().getXYPlot().removeAnnotation(annotation);
363 }
364 }
365 FundamentalDiagram.this.timeInfo = "";
366 }
367 }
368 };
369 }
370
371
372 @Override
373 protected void addPopUpMenuItems(final JPopupMenu popupMenu)
374 {
375 super.addPopUpMenuItems(popupMenu);
376 popupMenu.insert(new JPopupMenu.Separator(), 0);
377
378 JMenu updMenu = new JMenu("Update frequency");
379 ButtonGroup updGroup = new ButtonGroup();
380 for (int f : this.source.getPossibleUpdateFrequencies())
381 {
382 String format = "%dx";
383 JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, f));
384 item.setSelected(f == 1);
385 item.addActionListener(new ActionListener()
386 {
387
388 @SuppressWarnings("synthetic-access")
389 @Override
390 public void actionPerformed(final ActionEvent e)
391 {
392
393 if ((int) (.5 + FundamentalDiagram.this.source.getAggregationPeriod().si
394 / FundamentalDiagram.this.source.getUpdateInterval().si) != f)
395 {
396 Duration interval = Duration.createSI(FundamentalDiagram.this.source.getAggregationPeriod().si / f);
397 FundamentalDiagram.this.setUpdateInterval(interval);
398
399
400 FundamentalDiagram.this.source.setUpdateInterval(interval,
401 FundamentalDiagram.this.getUpdateTime().plus(interval.multiplyBy(0.5)),
402 FundamentalDiagram.this);
403 getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
404 getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
405 notifyPlotChange();
406 }
407 }
408 });
409 updGroup.add(item);
410 updMenu.add(item);
411 }
412 popupMenu.insert(updMenu, 0);
413
414 JMenu aggMenu = new JMenu("Aggregation period");
415 ButtonGroup aggGroup = new ButtonGroup();
416 for (double t : this.source.getPossibleAggregationPeriods())
417 {
418 double t2 = t;
419 String format = "%.0f s";
420 if (t >= 60.0)
421 {
422 t2 = t / 60.0;
423 format = "%.0f min";
424 }
425 JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, t2));
426 item.setSelected(t == this.source.getAggregationPeriod().si);
427 item.addActionListener(new ActionListener()
428 {
429
430
431 @SuppressWarnings("synthetic-access")
432 @Override
433 public void actionPerformed(final ActionEvent e)
434 {
435 if (FundamentalDiagram.this.source.getAggregationPeriod().si != t)
436 {
437 int n = (int) (0.5 + FundamentalDiagram.this.source.getAggregationPeriod().si
438 / FundamentalDiagram.this.source.getUpdateInterval().si);
439 Duration period = Duration.createSI(t);
440 FundamentalDiagram.this.setUpdateInterval(period.divideBy(n));
441
442 FundamentalDiagram.this.source.setAggregationPeriod(period);
443 FundamentalDiagram.this.source.setUpdateInterval(period.divideBy(n),
444 FundamentalDiagram.this.getUpdateTime().plus(period.divideBy(n).multiplyBy(0.5)),
445 FundamentalDiagram.this);
446 getChart().getXYPlot().zoomDomainAxes(0.0, null, null);
447 getChart().getXYPlot().zoomRangeAxes(0.0, null, null);
448 notifyPlotChange();
449 }
450 }
451
452 });
453 aggGroup.add(item);
454 aggMenu.add(item);
455 }
456 popupMenu.insert(aggMenu, 0);
457 }
458
459
460 @Override
461 protected void increaseTime(final Time time)
462 {
463 if (this.graphUpdater != null && time.si >= this.source.getAggregationPeriod().si)
464 {
465 this.graphUpdater.offer(time);
466 }
467 }
468
469
470 @Override
471 public int getSeriesCount()
472 {
473 if (this.source == null)
474 {
475 return 0;
476 }
477 return this.source.getNumberOfSeries();
478 }
479
480
481 @Override
482 public Comparable<String> getSeriesKey(final int series)
483 {
484 return this.seriesLabels.get(series);
485 }
486
487
488 @SuppressWarnings("rawtypes")
489 @Override
490 public int indexOf(final Comparable seriesKey)
491 {
492 int index = this.seriesLabels.indexOf(seriesKey);
493 return index < 0 ? 0 : index;
494 }
495
496
497 @Override
498 public DomainOrder getDomainOrder()
499 {
500 return DomainOrder.NONE;
501 }
502
503
504 @Override
505 public int getItemCount(final int series)
506 {
507 return this.source.getItemCount(series);
508 }
509
510
511 @Override
512 public Number getX(final int series, final int item)
513 {
514 return getXValue(series, item);
515 }
516
517
518 @Override
519 public double getXValue(final int series, final int item)
520 {
521 return this.domainQuantity.getValue(this.source, series, item);
522 }
523
524
525 @Override
526 public Number getY(final int series, final int item)
527 {
528 return getYValue(series, item);
529 }
530
531
532 @Override
533 public double getYValue(final int series, final int item)
534 {
535 return this.rangeQuantity.getValue(this.source, series, item);
536 }
537
538
539 @Override
540 public GraphType getGraphType()
541 {
542 return GraphType.FUNDAMENTAL_DIAGRAM;
543 }
544
545
546 @Override
547 protected String getStatusLabel(final double domainValue, final double rangeValue)
548 {
549 return this.domainQuantity.format(domainValue) + ", " + this.rangeQuantity.format(rangeValue) + ", "
550 + this.otherQuantity.format(this.domainQuantity.computeOther(this.rangeQuantity, domainValue, rangeValue))
551 + this.timeInfo;
552 }
553
554
555
556
557
558
559
560
561
562
563
564
565
566 public enum Quantity
567 {
568
569 DENSITY
570 {
571
572 @Override
573 public String label()
574 {
575 return "Density [veh/km] \u2192";
576 }
577
578
579 @Override
580 public String format(final double value)
581 {
582 return String.format("%.0f veh/km", value);
583 }
584
585
586 @Override
587 public double getValue(final FdSource src, final int series, final int item)
588 {
589 return 1000 * src.getDensity(series, item);
590 }
591
592
593 @Override
594 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
595 {
596
597 return pairing.equals(FLOW) ? pairedValue / thisValue : thisValue * pairedValue;
598 }
599 },
600
601
602 FLOW
603 {
604
605 @Override
606 public String label()
607 {
608 return "Flow [veh/h] \u2192";
609 }
610
611
612 @Override
613 public String format(final double value)
614 {
615 return String.format("%.0f veh/h", value);
616 }
617
618
619 @Override
620 public double getValue(final FdSource src, final int series, final int item)
621 {
622 return 3600 * src.getFlow(series, item);
623 }
624
625
626 @Override
627 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
628 {
629
630 return thisValue / pairedValue;
631 }
632 },
633
634
635 SPEED
636 {
637
638 @Override
639 public String label()
640 {
641 return "Speed [km/h] \u2192";
642 }
643
644
645 @Override
646 public String format(final double value)
647 {
648 return String.format("%.1f km/h", value);
649 }
650
651
652 @Override
653 public double getValue(final FdSource src, final int series, final int item)
654 {
655 return 3.6 * src.getSpeed(series, item);
656 }
657
658
659 @Override
660 public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
661 {
662
663 return pairing.equals(DENSITY) ? thisValue * pairedValue : pairedValue / thisValue;
664 }
665 };
666
667
668
669
670
671 public abstract String label();
672
673
674
675
676
677
678 public abstract String format(double value);
679
680
681
682
683
684
685
686
687 public abstract double getValue(FdSource src, int series, int item);
688
689
690
691
692
693
694
695
696 public abstract double computeOther(Quantity pairing, double thisValue, double pairedValue);
697
698 }
699
700
701
702
703
704
705
706
707
708
709
710
711
712 public interface FdSource
713 {
714
715
716
717
718 default double[] getPossibleAggregationPeriods()
719 {
720 return DEFAULT_PERIODS;
721 }
722
723
724
725
726
727 default int[] getPossibleUpdateFrequencies()
728 {
729 return DEFAULT_UPDATE_FREQUENCIES;
730 }
731
732
733
734
735
736 Duration getUpdateInterval();
737
738
739
740
741
742
743
744 void setUpdateInterval(Duration interval, Time time, FundamentalDiagram fd);
745
746
747
748
749
750 Duration getAggregationPeriod();
751
752
753
754
755
756 void setAggregationPeriod(Duration period);
757
758
759
760
761
762 Duration getDelay();
763
764
765
766
767
768 void increaseTime(Time time);
769
770
771
772
773
774 int getNumberOfSeries();
775
776
777
778
779
780
781 String getName(int series);
782
783
784
785
786
787
788 int getItemCount(int series);
789
790
791
792
793
794
795
796 double getFlow(int series, int item);
797
798
799
800
801
802
803
804 double getDensity(int series, int item);
805
806
807
808
809
810
811
812 double getSpeed(int series, int item);
813 }
814
815
816
817
818
819
820
821
822
823
824 @SuppressWarnings("methodlength")
825 public static FdSource sourceFromSampler(final Sampler<?> sampler, final GraphCrossSection<KpiLaneDirection> crossSection,
826 final boolean aggregateLanes, final Duration aggregationTime, final boolean harmonic)
827 {
828 return new CrossSectionSamplerFdSource<>(sampler, crossSection, aggregateLanes, aggregationTime, harmonic);
829 }
830
831
832
833
834
835
836
837
838
839 public static FdSource sourceFromSampler(final Sampler<?> sampler, final GraphPath<KpiLaneDirection> path,
840 final boolean aggregateLanes, final Duration aggregationTime)
841 {
842 return new PathSamplerFdSource<>(sampler, path, aggregateLanes, aggregationTime);
843 }
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858 private static class CrossSectionSamplerFdSource<S extends GraphCrossSection<? extends KpiLaneDirection>>
859 extends AbstractSpaceSamplerFdSource<S>
860 {
861
862 private final boolean harmonic;
863
864
865
866
867
868
869
870
871
872 CrossSectionSamplerFdSource(final Sampler<?> sampler, final S crossSection, final boolean aggregateLanes,
873 final Duration aggregationPeriod, final boolean harmonic)
874 {
875 super(sampler, crossSection, aggregateLanes, aggregationPeriod);
876 this.harmonic = harmonic;
877 }
878
879
880 @Override
881 protected void getMeasurements(final Trajectory<?> trajectory, final Time startTime, final Time endTime,
882 final Length length, final int series, final double[] measurements)
883 {
884 Length x = getSpace().position(series);
885 if (GraphUtil.considerTrajectory(trajectory, x, x))
886 {
887
888 Time t = trajectory.getTimeAtPosition(x);
889 if (t.si >= startTime.si && t.si < endTime.si)
890 {
891 measurements[0] = 1;
892 measurements[1] =
893 this.harmonic ? 1.0 / trajectory.getSpeedAtPosition(x).si : trajectory.getSpeedAtPosition(x).si;
894 }
895 }
896 }
897
898
899 @Override
900 protected double getVehicleCount(final double first, final double second)
901 {
902 return first;
903 }
904
905
906 @Override
907 protected double getSpeed(final double first, final double second)
908 {
909 return this.harmonic ? first / second : second / first;
910 }
911
912
913 @Override
914 public String toString()
915 {
916 return "CrossSectionSamplerFdSource [harmonic=" + this.harmonic + "]";
917 }
918
919 }
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934 private static class PathSamplerFdSource<S extends GraphPath<? extends KpiLaneDirection>>
935 extends AbstractSpaceSamplerFdSource<S>
936 {
937
938
939
940
941
942
943
944 PathSamplerFdSource(final Sampler<?> sampler, final S path, final boolean aggregateLanes,
945 final Duration aggregationPeriod)
946 {
947 super(sampler, path, aggregateLanes, aggregationPeriod);
948 }
949
950
951 @Override
952 protected void getMeasurements(final Trajectory<?> trajectory, final Time startTime, final Time endTime,
953 final Length length, final int sereies, final double[] measurements)
954 {
955 SpaceTimeView stv = trajectory.getSpaceTimeView(Length.ZERO, length, startTime, endTime);
956 measurements[0] = stv.getDistance().si;
957 measurements[1] = stv.getTime().si;
958 }
959
960
961 @Override
962 protected double getVehicleCount(final double first, final double second)
963 {
964 return first / getSpace().getTotalLength().si;
965 }
966
967
968 @Override
969 protected double getSpeed(final double first, final double second)
970 {
971 return first / second;
972 }
973
974
975 @Override
976 public String toString()
977 {
978 return "PathSamplerFdSource []";
979 }
980
981 }
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996 private abstract static class AbstractSpaceSamplerFdSource<S extends AbstractGraphSpace<? extends KpiLaneDirection>>
997 implements FdSource
998 {
999
1000 private int periodNumber = -1;
1001
1002
1003 private Duration updateInterval;
1004
1005
1006 private Duration aggregationPeriod;
1007
1008
1009 private final int nSeries;
1010
1011
1012 private double[][] firstMeasurement;
1013
1014
1015 private double[][] secondMeasurement;
1016
1017
1018 private boolean invalid = false;
1019
1020
1021 private final Sampler<?> sampler;
1022
1023
1024 private final S space;
1025
1026
1027 private final boolean aggregateLanes;
1028
1029
1030 private Map<KpiLaneDirection, Integer> lastConsecutivelyAssignedTrajectories = new LinkedHashMap<>();
1031
1032
1033 private Map<KpiLaneDirection, SortedSet<Integer>> assignedTrajectories = new LinkedHashMap<>();
1034
1035
1036
1037
1038
1039
1040
1041
1042 AbstractSpaceSamplerFdSource(final Sampler<?> sampler, final S space, final boolean aggregateLanes,
1043 final Duration aggregationPeriod)
1044 {
1045 this.sampler = sampler;
1046 this.space = space;
1047 this.aggregateLanes = aggregateLanes;
1048 this.nSeries = aggregateLanes ? 1 : space.getNumberOfSeries();
1049
1050 for (KpiLaneDirection laneDirection : space)
1051 {
1052 sampler.registerSpaceTimeRegion(new SpaceTimeRegion(laneDirection, Length.ZERO,
1053 laneDirection.getLaneData().getLength(), Time.ZERO, Time.createSI(Double.MAX_VALUE)));
1054
1055
1056 this.lastConsecutivelyAssignedTrajectories.put(laneDirection, -1);
1057 this.assignedTrajectories.put(laneDirection, new TreeSet<>());
1058 }
1059
1060 this.updateInterval = aggregationPeriod;
1061 this.aggregationPeriod = aggregationPeriod;
1062 this.firstMeasurement = new double[this.nSeries][10];
1063 this.secondMeasurement = new double[this.nSeries][10];
1064 }
1065
1066
1067
1068
1069
1070 protected S getSpace()
1071 {
1072 return this.space;
1073 }
1074
1075
1076 @Override
1077 public Duration getUpdateInterval()
1078 {
1079 return this.updateInterval;
1080 }
1081
1082
1083 @Override
1084 public void setUpdateInterval(final Duration interval, final Time time, final FundamentalDiagram fd)
1085 {
1086 if (this.updateInterval != interval)
1087 {
1088 this.updateInterval = interval;
1089 recalculate(time, fd);
1090 }
1091 }
1092
1093
1094 @Override
1095 public Duration getAggregationPeriod()
1096 {
1097 return this.aggregationPeriod;
1098 }
1099
1100
1101 @Override
1102 public void setAggregationPeriod(final Duration period)
1103 {
1104 if (this.aggregationPeriod != period)
1105 {
1106 this.aggregationPeriod = period;
1107 }
1108 }
1109
1110
1111
1112
1113
1114
1115 private void recalculate(final Time time, final FundamentalDiagram fd)
1116 {
1117 new Thread(new Runnable()
1118 {
1119 @SuppressWarnings("synthetic-access")
1120 public void run()
1121 {
1122 synchronized (AbstractSpaceSamplerFdSource.this)
1123 {
1124
1125 AbstractSpaceSamplerFdSource.this.invalid = true;
1126 AbstractSpaceSamplerFdSource.this.periodNumber = -1;
1127 AbstractSpaceSamplerFdSource.this.updateInterval = getUpdateInterval();
1128 AbstractSpaceSamplerFdSource.this.firstMeasurement =
1129 new double[AbstractSpaceSamplerFdSource.this.nSeries][10];
1130 AbstractSpaceSamplerFdSource.this.secondMeasurement =
1131 new double[AbstractSpaceSamplerFdSource.this.nSeries][10];
1132 AbstractSpaceSamplerFdSource.this.lastConsecutivelyAssignedTrajectories.clear();
1133 AbstractSpaceSamplerFdSource.this.assignedTrajectories.clear();
1134 for (KpiLaneDirection lane : AbstractSpaceSamplerFdSource.this.space)
1135 {
1136 AbstractSpaceSamplerFdSource.this.lastConsecutivelyAssignedTrajectories.put(lane, -1);
1137 AbstractSpaceSamplerFdSource.this.assignedTrajectories.put(lane, new TreeSet<>());
1138 }
1139 while ((AbstractSpaceSamplerFdSource.this.periodNumber + 1) * getUpdateInterval().si
1140 + AbstractSpaceSamplerFdSource.this.aggregationPeriod.si <= time.si)
1141 {
1142 increaseTime(
1143 Time.createSI((AbstractSpaceSamplerFdSource.this.periodNumber + 1) * getUpdateInterval().si
1144 + AbstractSpaceSamplerFdSource.this.aggregationPeriod.si));
1145 fd.notifyPlotChange();
1146 }
1147 AbstractSpaceSamplerFdSource.this.invalid = false;
1148 }
1149 }
1150 }, "Fundamental diagram recalculation").start();
1151 }
1152
1153
1154 @Override
1155 public Duration getDelay()
1156 {
1157 return Duration.createSI(1.0);
1158 }
1159
1160
1161 @Override
1162 public synchronized void increaseTime(final Time time)
1163 {
1164 if (time.si < this.aggregationPeriod.si)
1165 {
1166
1167 return;
1168 }
1169
1170
1171 int nextPeriod = this.periodNumber + 1;
1172 if (nextPeriod >= this.firstMeasurement[0].length - 1)
1173 {
1174 for (int i = 0; i < this.nSeries; i++)
1175 {
1176 this.firstMeasurement[i] = GraphUtil.ensureCapacity(this.firstMeasurement[i], nextPeriod + 1);
1177 this.secondMeasurement[i] = GraphUtil.ensureCapacity(this.secondMeasurement[i], nextPeriod + 1);
1178 }
1179 }
1180
1181
1182 Time startTime = time.minus(this.aggregationPeriod);
1183 double first = 0;
1184 double second = 0.0;
1185 for (int series = 0; series < this.space.getNumberOfSeries(); series++)
1186 {
1187 Iterator<? extends KpiLaneDirection> it = this.space.iterator(series);
1188 while (it.hasNext())
1189 {
1190 KpiLaneDirection lane = it.next();
1191 TrajectoryGroup<?> trajectoryGroup = this.sampler.getTrajectoryGroup(lane);
1192 int last = this.lastConsecutivelyAssignedTrajectories.get(lane);
1193 SortedSet<Integer> assigned = this.assignedTrajectories.get(lane);
1194 if (!this.aggregateLanes)
1195 {
1196 first = 0.0;
1197 second = 0.0;
1198 }
1199
1200
1201 int i = 0;
1202 for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories())
1203 {
1204
1205 try
1206 {
1207 if (i > last && !assigned.contains(i))
1208 {
1209
1210 if (GraphUtil.considerTrajectory(trajectory, startTime, time))
1211 {
1212 double[] measurements = new double[2];
1213 getMeasurements(trajectory, startTime, time, lane.getLaneData().getLength(), series,
1214 measurements);
1215 first += measurements[0];
1216 second += measurements[1];
1217 }
1218 if (trajectory.getT(trajectory.size() - 1) < startTime.si - getDelay().si)
1219 {
1220 assigned.add(i);
1221 }
1222 }
1223 i++;
1224 }
1225 catch (SamplingException exception)
1226 {
1227 throw new RuntimeException("Unexpected exception while counting trajectories.", exception);
1228 }
1229 }
1230 if (!this.aggregateLanes)
1231 {
1232 this.firstMeasurement[series][nextPeriod] = first;
1233 this.secondMeasurement[series][nextPeriod] = second;
1234 }
1235
1236
1237 if (!assigned.isEmpty())
1238 {
1239 int possibleNextLastAssigned = assigned.first();
1240 while (possibleNextLastAssigned == last + 1)
1241 {
1242 last = possibleNextLastAssigned;
1243 assigned.remove(possibleNextLastAssigned);
1244 possibleNextLastAssigned = assigned.isEmpty() ? -1 : assigned.first();
1245 }
1246 this.lastConsecutivelyAssignedTrajectories.put(lane, last);
1247 }
1248 }
1249 }
1250 if (this.aggregateLanes)
1251 {
1252
1253 this.firstMeasurement[0][nextPeriod] = first / this.space.getNumberOfSeries();
1254 this.secondMeasurement[0][nextPeriod] = second / this.space.getNumberOfSeries();
1255 }
1256 this.periodNumber = nextPeriod;
1257 }
1258
1259
1260 @Override
1261 public int getNumberOfSeries()
1262 {
1263
1264
1265 this.invalid = false;
1266 return this.nSeries;
1267 }
1268
1269
1270 @Override
1271 public String getName(final int series)
1272 {
1273 if (this.aggregateLanes)
1274 {
1275 return "Aggregate";
1276 }
1277 return this.space.getName(series);
1278 }
1279
1280
1281 @Override
1282 public int getItemCount(final int series)
1283 {
1284 return this.periodNumber + 1;
1285 }
1286
1287
1288 @Override
1289 public final double getFlow(final int series, final int item)
1290 {
1291 if (this.invalid)
1292 {
1293 return Double.NaN;
1294 }
1295 return getVehicleCount(this.firstMeasurement[series][item], this.secondMeasurement[series][item])
1296 / this.aggregationPeriod.si;
1297 }
1298
1299
1300 @Override
1301 public final double getDensity(final int series, final int item)
1302 {
1303 return getFlow(series, item) / getSpeed(series, item);
1304 }
1305
1306
1307 @Override
1308 public final double getSpeed(final int series, final int item)
1309 {
1310 if (this.invalid)
1311 {
1312 return Double.NaN;
1313 }
1314 return getSpeed(this.firstMeasurement[series][item], this.secondMeasurement[series][item]);
1315 }
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328 protected abstract void getMeasurements(Trajectory<?> trajectory, Time startTime, Time endTime, Length length,
1329 int series, double[] measurements);
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342 protected abstract double getVehicleCount(double first, double second);
1343
1344
1345
1346
1347
1348
1349
1350
1351 protected abstract double getSpeed(double first, double second);
1352
1353 }
1354
1355
1356 @Override
1357 public String toString()
1358 {
1359 return "FundamentalDiagram [source=" + this.source + ", domainQuantity=" + this.domainQuantity + ", rangeQuantity="
1360 + this.rangeQuantity + ", otherQuantity=" + this.otherQuantity + ", seriesLabels=" + this.seriesLabels
1361 + ", graphUpdater=" + this.graphUpdater + ", timeInfo=" + this.timeInfo + ", legend=" + this.legend
1362 + ", laneVisible=" + this.laneVisible + "]";
1363 }
1364
1365 }