View Javadoc
1   package org.opentrafficsim.demo.fd;
2   
3   import java.awt.Color;
4   import java.awt.Component;
5   import java.awt.Container;
6   import java.awt.Dimension;
7   import java.awt.event.ActionEvent;
8   import java.awt.event.ActionListener;
9   import java.util.ArrayList;
10  import java.util.Dictionary;
11  import java.util.Hashtable;
12  import java.util.LinkedHashMap;
13  import java.util.LinkedHashSet;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Set;
17  import java.util.function.Supplier;
18  
19  import javax.swing.Box;
20  import javax.swing.BoxLayout;
21  import javax.swing.JButton;
22  import javax.swing.JComboBox;
23  import javax.swing.JLabel;
24  import javax.swing.JPanel;
25  import javax.swing.JPopupMenu;
26  import javax.swing.JSlider;
27  import javax.swing.border.EmptyBorder;
28  import javax.swing.event.ChangeEvent;
29  import javax.swing.event.ChangeListener;
30  
31  import org.djunits.unit.FrequencyUnit;
32  import org.djunits.unit.SpeedUnit;
33  import org.djunits.value.vdouble.scalar.Acceleration;
34  import org.djunits.value.vdouble.scalar.Direction;
35  import org.djunits.value.vdouble.scalar.Duration;
36  import org.djunits.value.vdouble.scalar.Frequency;
37  import org.djunits.value.vdouble.scalar.Length;
38  import org.djunits.value.vdouble.scalar.Speed;
39  import org.djutils.cli.CliUtil;
40  import org.djutils.draw.point.Point2d;
41  import org.djutils.exceptions.Try;
42  import org.djutils.math.means.HarmonicMean;
43  import org.opentrafficsim.animation.data.util.GraphLaneUtil;
44  import org.opentrafficsim.animation.graphs.FdDataSource;
45  import org.opentrafficsim.animation.graphs.FundamentalDiagram;
46  import org.opentrafficsim.animation.graphs.GraphCrossSection;
47  import org.opentrafficsim.animation.graphs.GraphPath;
48  import org.opentrafficsim.animation.graphs.PlotScheduler;
49  import org.opentrafficsim.animation.graphs.TrajectoryPlot;
50  import org.opentrafficsim.animation.graphs.FundamentalDiagram.FdLine;
51  import org.opentrafficsim.animation.graphs.FundamentalDiagram.Quantity;
52  import org.opentrafficsim.base.OtsRuntimeException;
53  import org.opentrafficsim.base.logger.Logger;
54  import org.opentrafficsim.base.parameters.ParameterException;
55  import org.opentrafficsim.base.parameters.ParameterTypes;
56  import org.opentrafficsim.base.parameters.Parameters;
57  import org.opentrafficsim.core.definitions.Defaults;
58  import org.opentrafficsim.core.definitions.DefaultsNl;
59  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
60  import org.opentrafficsim.core.gtu.Gtu;
61  import org.opentrafficsim.core.gtu.GtuErrorHandler;
62  import org.opentrafficsim.core.gtu.GtuException;
63  import org.opentrafficsim.core.gtu.GtuType;
64  import org.opentrafficsim.core.idgenerator.IdSupplier;
65  import org.opentrafficsim.core.network.LinkPosition;
66  import org.opentrafficsim.core.network.LinkType;
67  import org.opentrafficsim.core.network.Network;
68  import org.opentrafficsim.core.network.NetworkException;
69  import org.opentrafficsim.core.network.Node;
70  import org.opentrafficsim.core.parameters.ParameterFactory;
71  import org.opentrafficsim.road.definitions.DefaultsRoadNl;
72  import org.opentrafficsim.road.gtu.LaneBookkeeping;
73  import org.opentrafficsim.road.gtu.VehicleModel;
74  import org.opentrafficsim.road.gtu.generator.CfBaRoomChecker;
75  import org.opentrafficsim.road.gtu.generator.GeneratorPositions;
76  import org.opentrafficsim.road.gtu.generator.GeneratorPositions.LaneBias;
77  import org.opentrafficsim.road.gtu.generator.GeneratorPositions.LaneBiases;
78  import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator;
79  import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator.RoomChecker;
80  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristics;
81  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGenerator;
82  import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalRoutePlannerFactory;
83  import org.opentrafficsim.road.gtu.tactical.LaneBasedTacticalPlannerFactory;
84  import org.opentrafficsim.road.gtu.tactical.lmrs.Lmrs;
85  import org.opentrafficsim.road.gtu.tactical.lmrs.LmrsFactory;
86  import org.opentrafficsim.road.network.CrossSectionLink;
87  import org.opentrafficsim.road.network.Lane;
88  import org.opentrafficsim.road.network.LaneKeepingPolicy;
89  import org.opentrafficsim.road.network.LanePosition;
90  import org.opentrafficsim.road.network.LaneType;
91  import org.opentrafficsim.road.network.RoadNetwork;
92  import org.opentrafficsim.road.network.factory.LaneFactory;
93  import org.opentrafficsim.road.network.object.detector.SinkDetector;
94  import org.opentrafficsim.road.network.sampling.LaneDataRoad;
95  import org.opentrafficsim.road.network.sampling.RoadSampler;
96  import org.opentrafficsim.road.network.speed.LaneSpeedLimits;
97  import org.opentrafficsim.swing.graphs.OtsPlotScheduler;
98  import org.opentrafficsim.swing.graphs.SwingFundamentalDiagram;
99  import org.opentrafficsim.swing.graphs.SwingTrajectoryPlot;
100 import org.opentrafficsim.swing.gui.OtsSimulationPanel;
101 import org.opentrafficsim.swing.gui.OtsSimulationPanel.DemoPanelPosition;
102 import org.opentrafficsim.swing.gui.OtsSimulationPanelDecorator;
103 import org.opentrafficsim.swing.script.AbstractSimulationScript;
104 
105 import nl.tudelft.simulation.dsol.swing.gui.TablePanel;
106 import nl.tudelft.simulation.jstats.distributions.DistNormal;
107 import nl.tudelft.simulation.jstats.streams.StreamInterface;
108 
109 /**
110  * Demo showing what fundamental diagrams are. This demo is for education purposes.
111  * <p>
112  * Copyright (c) 2020-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
113  * BSD-style license. See <a href="https://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
114  * </p>
115  * @author Alexander Verbraeck
116  * @author Peter Knoppers
117  * @author Wouter Schakel
118  */
119 public class FundamentalDiagramDemo extends AbstractSimulationScript
120 {
121 
122     /** Dynamic demand. */
123     private Frequency demand = new Frequency(3500.0, FrequencyUnit.PER_HOUR);
124 
125     /** Dynamic truck fraction. */
126     private double truckFraction = 0.05;
127 
128     /** Speed limit. */
129     private LaneSpeedLimits speedLimits = new LaneSpeedLimits(new Speed(120.0, SpeedUnit.KM_PER_HOUR),
130             Map.of(DefaultsNl.TRUCK, new Speed(80.0, SpeedUnit.KM_PER_HOUR)));
131 
132     /** Tmin. */
133     private Duration tMin = Duration.ofSI(0.56);
134 
135     /** Tmax. */
136     private Duration tMax = Duration.ofSI(1.2);
137 
138     /** Panel splitting controls from graphs. */
139     private JPanel splitPanel;
140 
141     /** Panel with graphs. */
142     private TablePanel graphPanel;
143 
144     /** Sampler. */
145     private RoadSampler sampler;
146 
147     /** Selected cross-section. */
148     private String absoluteCrossSection1 = "1.50";
149 
150     /** Second selected cross-section. */
151     private String absoluteCrossSection2 = "None";
152 
153     /** Third selected cross-section. */
154     private String absoluteCrossSection3 = "None";
155 
156     /** Fd line in graphs based on settings. */
157     private DynamicFdLine fdLine = new DynamicFdLine();
158 
159     /** Trajectory plot. */
160     private TrajectoryPlot trajectoryPlot;
161 
162     /** Fundamental diagrams that are updated when a setting is changed. */
163     private Set<FundamentalDiagram> funamentalDiagrams = new LinkedHashSet<>();
164 
165     /** Sources by name for each cross-section. */
166     private Map<String, FdDataSource> fdSourceMap = new LinkedHashMap<>();
167 
168     /** Panel of trajectory graph. */
169     private Container trajectoryPanel;
170 
171     /** Plot scheduler. */
172     private PlotScheduler scheduler;
173 
174     /**
175      * Constructor.
176      */
177     public FundamentalDiagramDemo()
178     {
179         super("FD Demo", "Fundamental diagram demo");
180     }
181 
182     /**
183      * Main program.
184      * @param args the command line arguments (not used)
185      */
186     public static void main(final String[] args)
187     {
188         FundamentalDiagramDemo demo = new FundamentalDiagramDemo();
189         try
190         {
191             CliUtil.changeOptionDefault(demo, "simulationTime", "360000s"); // 100h
192             CliUtil.execute(demo, args);
193             demo.start();
194         }
195         catch (Exception exception)
196         {
197             exception.printStackTrace();
198         }
199     }
200 
201     @Override
202     protected RoadNetwork setupSimulation(final OtsSimulatorInterface sim) throws Exception
203     {
204         // Network
205         RoadNetwork network = new RoadNetwork("FD demo network", sim);
206         GtuType car = DefaultsNl.CAR;
207         GtuType truck = DefaultsNl.TRUCK;
208 
209         Node nodeA = new Node(network, "Origin", new Point2d(0.0, 0.0), Direction.ZERO);
210         Node nodeB = new Node(network, "Lane-drop", new Point2d(1500.0, 0.0), Direction.ZERO);
211         Node nodeC = new Node(network, "Destination", new Point2d(2500.0, 0.0), Direction.ZERO);
212 
213         LinkType linkType = DefaultsNl.FREEWAY;
214         LaneKeepingPolicy policy = LaneKeepingPolicy.KEEPRIGHT;
215         Length laneWidth = Length.ofSI(3.5);
216         LaneType laneType = DefaultsRoadNl.FREEWAY;
217 
218         List<Lane> lanesAB = new LaneFactory(network, nodeA, nodeB, linkType, sim, policy)
219                 .leftToRight(3.0, laneWidth, laneType, this.speedLimits).addLanes(DefaultsRoadNl.DASHED, DefaultsRoadNl.DASHED)
220                 .getLanes();
221         List<Lane> lanesBC = new LaneFactory(network, nodeB, nodeC, linkType, sim, policy)
222                 .leftToRight(2.0, laneWidth, laneType, this.speedLimits).addLanes(DefaultsRoadNl.DASHED).getLanes();
223 
224         // Generator
225         // inter-arrival time generator
226         StreamInterface stream = sim.getModel().getStream("generation");
227         Supplier<Duration> interarrivelTimeGenerator = new Supplier<Duration>()
228         {
229             @Override
230             public Duration get()
231             {
232                 double mean = 1.0 / FundamentalDiagramDemo.this.demand.si;
233                 return Duration.ofSI(-mean * Math.log(stream.nextDouble()));
234             }
235         };
236         // GTU characteristics generator
237         LaneBasedTacticalPlannerFactory<Lmrs> tacticalPlannerFactory = new LmrsFactory<>(Lmrs::new).setStream(stream);
238         DistNormal fSpeed = new DistNormal(stream, 123.7 / 120.0, 12.0 / 120.0);
239         DistNormal fSpeedGtu = new DistNormal(stream, 85.0 / 80.0, 2.5 / 80.0);
240         ParameterFactory parametersFactory = new ParameterFactory()
241         {
242             @Override
243             public void setValues(final Parameters parameters, final GtuType gtuType) throws ParameterException
244             {
245                 if (gtuType.equals(truck))
246                 {
247                     parameters.setParameter(ParameterTypes.A, Acceleration.ofSI(0.4));
248                 }
249                 else
250                 {
251                     parameters.setParameter(ParameterTypes.A, Acceleration.ofSI(2.0));
252                 }
253                 parameters.setParameter(ParameterTypes.FSPEED, fSpeed.draw()); // also for trucks due to low speed limit option
254                 parameters.setParameter(ParameterTypes.FSPEED_GTU, fSpeedGtu.draw());
255                 parameters.setParameter(ParameterTypes.TMIN, FundamentalDiagramDemo.this.tMin);
256                 parameters.setParameter(ParameterTypes.TMAX, FundamentalDiagramDemo.this.tMax);
257             }
258         };
259         LaneBasedStrategicalRoutePlannerFactory laneBasedStrategicalPlannerFactory =
260                 new LaneBasedStrategicalRoutePlannerFactory(tacticalPlannerFactory, parametersFactory);
261         LaneBasedGtuCharacteristicsGenerator laneBasedGtuCharacteristicsGenerator = new LaneBasedGtuCharacteristicsGenerator()
262         {
263             @Override
264             public LaneBasedGtuCharacteristics draw() throws ParameterException, GtuException
265             {
266                 GtuType gtuType = stream.nextDouble() > FundamentalDiagramDemo.this.truckFraction ? car : truck;
267 
268                 return new LaneBasedGtuCharacteristics(Defaults.NL.apply(gtuType, stream)
269                         .orElseThrow(
270                                 () -> new GtuException("No characteristics for GTU type " + gtuType + " could be generated."))
271                         .get(), laneBasedStrategicalPlannerFactory, null, nodeA, nodeC, VehicleModel.MINMAX);
272             }
273         };
274         // generator positions
275         Set<LanePosition> initialPosition = new LinkedHashSet<>();
276         for (Lane lane : lanesAB)
277         {
278             initialPosition.add(new LanePosition(lane, Length.ZERO));
279         }
280         LaneBiases biases = new LaneBiases();
281         biases.addBias(car, LaneBias.bySpeed(new Speed(130.0, SpeedUnit.KM_PER_HOUR), new Speed(70.0, SpeedUnit.KM_PER_HOUR)));
282         biases.addBias(truck, LaneBias.TRUCK_RIGHT);
283         GeneratorPositions generatorPositions = GeneratorPositions.create(initialPosition, stream, biases);
284         // room checker
285         RoomChecker roomChecker = new CfBaRoomChecker();
286         // id generator
287         IdSupplier idGenerator = new IdSupplier("");
288         // generator
289         LaneBasedGtuGenerator generator = new LaneBasedGtuGenerator("generator", interarrivelTimeGenerator,
290                 laneBasedGtuCharacteristicsGenerator, generatorPositions, network, sim, roomChecker, idGenerator);
291         generator.setErrorHandler(GtuErrorHandler.DELETE);
292         generator.setBookkeeping(LaneBookkeeping.INSTANT);
293         generator.setNoLaneChangeDistance(Length.ofSI(100.0));
294 
295         // Sinks
296         for (Lane lane : lanesBC)
297         {
298             new SinkDetector(lane, lane.getLength(), DefaultsNl.ROAD_USERS);
299         }
300 
301         return network;
302     }
303 
304     @Override
305     protected OtsSimulationPanelDecorator getDecorator()
306     {
307         return new OtsSimulationPanelDecorator()
308         {
309             @Override
310             public void setupDemo(final OtsSimulationPanel simulationPanel, final Network network)
311             {
312                 FundamentalDiagramDemo.this.setupDemo(simulationPanel, network);
313             }
314         };
315     }
316 
317     /**
318      * Setup a demo panel within the animation panel. The default implementation does nothing.
319      * @param animationPanel animation panel
320      * @param network network
321      */
322     private void setupDemo(final OtsSimulationPanel animationPanel, final Network network)
323     {
324         this.fdLine.update();
325 
326         // Demo panel
327         animationPanel.createDemoPanel(DemoPanelPosition.BOTTOM);
328         animationPanel.getDemoPanel().setPreferredSize(new Dimension(1000, 500));
329         this.splitPanel = new JPanel(); // controls vs. graphs
330         JPanel controlPanel = new JPanel();
331         this.splitPanel.setLayout(new BoxLayout(this.splitPanel, BoxLayout.X_AXIS));
332         this.splitPanel.add(controlPanel);
333         animationPanel.getDemoPanel().add(this.splitPanel);
334 
335         // Control panel
336         controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS));
337         controlPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
338         controlPanel.setPreferredSize(new Dimension(250, 500));
339         Dimension controlSize = new Dimension(250, 0);
340         int strutSize = 20;
341         setupDetectorSelectors(controlPanel, controlSize);
342         // spacer
343         controlPanel.add(Box.createVerticalStrut(strutSize));
344         // reset button
345         JButton reset = new JButton("Clear data & graphs");
346         reset.setAlignmentX(Component.CENTER_ALIGNMENT);
347         reset.addActionListener(new ActionListener()
348         {
349             @Override
350             public void actionPerformed(final ActionEvent e)
351             {
352                 clearDataAndGraphs();
353             }
354         });
355         controlPanel.add(reset);
356         // spacer
357         controlPanel.add(Box.createVerticalStrut(strutSize));
358         // demand
359         setupDemandSliders(controlPanel, controlSize, strutSize);
360         // spacer
361         controlPanel.add(Box.createVerticalStrut(strutSize));
362         setupParameterSliders(controlPanel, controlSize, strutSize);
363 
364         // Initiate graphs
365         this.scheduler = new OtsPlotScheduler(getSimulator());
366         clearDataAndGraphs();
367     }
368 
369     /**
370      * Sets up the detector selection drop-down menus.
371      * @param controlPanel control panel
372      * @param controlSize control size
373      */
374     private void setupDetectorSelectors(final JPanel controlPanel, final Dimension controlSize)
375     {
376         // cross section dropdown
377         JLabel crossSectionLabel = new JLabel("<html>Cross-section location [km]</html>");
378         crossSectionLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
379         crossSectionLabel.setMinimumSize(controlSize);
380         controlPanel.add(crossSectionLabel);
381         List<String> list = new ArrayList<>();
382         for (int i = 250; i <= 2250; i += 250)
383         {
384             list.add(String.format("%.2f", i / 1000.0));
385         }
386         JComboBox<String> crossSectionMenu = new JComboBox<String>(list.toArray(new String[0]));
387         Dimension crossSectionMenuSize = new Dimension(250, 25);
388         crossSectionMenu.setMinimumSize(crossSectionMenuSize);
389         crossSectionMenu.setMaximumSize(crossSectionMenuSize);
390         crossSectionMenu.setSelectedIndex(5);
391         crossSectionMenu.addActionListener(new ActionListener()
392         {
393             @SuppressWarnings({"unchecked"})
394             @Override
395             public void actionPerformed(final ActionEvent e)
396             {
397                 FundamentalDiagramDemo.this.absoluteCrossSection1 =
398                         ((String) ((JComboBox<String>) e.getSource()).getSelectedItem()).replace(",", ".");
399                 createFundamentalDiagramsForCrossSections();
400             }
401         });
402         controlPanel.add(crossSectionMenu);
403         // 2nd drop down
404         list = new ArrayList<>();
405         list.add("None");
406         for (int i = 250; i <= 2250; i += 250)
407         {
408             list.add(String.format("%.2f", i / 1000.0));
409         }
410         crossSectionMenu = new JComboBox<String>(list.toArray(new String[0]));
411         crossSectionMenu.setMinimumSize(crossSectionMenuSize);
412         crossSectionMenu.setMaximumSize(crossSectionMenuSize);
413         crossSectionMenu.setSelectedIndex(0);
414         crossSectionMenu.addActionListener(new ActionListener()
415         {
416             @SuppressWarnings({"unchecked"})
417             @Override
418             public void actionPerformed(final ActionEvent e)
419             {
420                 FundamentalDiagramDemo.this.absoluteCrossSection2 =
421                         ((String) ((JComboBox<String>) e.getSource()).getSelectedItem()).replace(",", ".");
422                 createFundamentalDiagramsForCrossSections();
423             }
424         });
425         controlPanel.add(crossSectionMenu);
426         // 3rd drop down
427         crossSectionMenu = new JComboBox<String>(list.toArray(new String[0]));
428         crossSectionMenu.setMinimumSize(crossSectionMenuSize);
429         crossSectionMenu.setMaximumSize(crossSectionMenuSize);
430         crossSectionMenu.setSelectedIndex(0);
431         crossSectionMenu.addActionListener(new ActionListener()
432         {
433             @SuppressWarnings({"unchecked"})
434             @Override
435             public void actionPerformed(final ActionEvent e)
436             {
437                 FundamentalDiagramDemo.this.absoluteCrossSection3 =
438                         ((String) ((JComboBox<String>) e.getSource()).getSelectedItem()).replace(",", ".");
439                 createFundamentalDiagramsForCrossSections();
440             }
441         });
442         controlPanel.add(crossSectionMenu);
443     }
444 
445     /**
446      * Sets up the parameter value sliders.
447      * @param controlPanel control panel
448      * @param controlSize control size
449      * @param strutSize space between parameters
450      */
451     private void setupDemandSliders(final JPanel controlPanel, final Dimension controlSize, final int strutSize)
452     {
453         JLabel demandLabel = new JLabel("<html>Demand [veh/h]</html>");
454         demandLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
455         demandLabel.setPreferredSize(controlSize);
456         controlPanel.add(demandLabel);
457         JSlider demandSlider = new JSlider(500, 5000, 3500);
458         demandSlider.setPreferredSize(controlSize);
459         demandSlider.setSnapToTicks(true);
460         demandSlider.setMinorTickSpacing(250);
461         demandSlider.setMajorTickSpacing(1000);
462         demandSlider.setPaintTicks(true);
463         demandSlider.setPaintLabels(true);
464         demandSlider.setToolTipText("<html>Demand [veh/h]</html>");
465         demandSlider.addChangeListener(new ChangeListener()
466         {
467             @Override
468             public void stateChanged(final ChangeEvent e)
469             {
470                 double value = ((JSlider) e.getSource()).getValue();
471                 FundamentalDiagramDemo.this.demand = new Frequency(value, FrequencyUnit.PER_HOUR);
472             }
473         });
474         controlPanel.add(demandSlider);
475         // spacer
476         controlPanel.add(Box.createVerticalStrut(strutSize));
477         // truck percentage
478         JLabel truckLabel = new JLabel("<html>Truck percentage [%]</html>");
479         truckLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
480         truckLabel.setPreferredSize(controlSize);
481         controlPanel.add(truckLabel);
482         JSlider truckSlider = new JSlider(0, 30, 5);
483         truckSlider.setPreferredSize(controlSize);
484         truckSlider.setSnapToTicks(true);
485         truckSlider.setMinorTickSpacing(5);
486         truckSlider.setMajorTickSpacing(10);
487         truckSlider.setPaintTicks(true);
488         truckSlider.setPaintLabels(true);
489         truckSlider.setToolTipText("<html>Truck percentage [%]</html>");
490         truckSlider.addChangeListener(new ChangeListener()
491         {
492             @Override
493             public void stateChanged(final ChangeEvent e)
494             {
495                 double value = ((JSlider) e.getSource()).getValue() / 100.0;
496                 FundamentalDiagramDemo.this.truckFraction = value;
497                 FundamentalDiagramDemo.this.fdLine.update();
498                 notifyPlotsChanged();
499             }
500         });
501         controlPanel.add(truckSlider);
502     }
503 
504     /**
505      * Sets up the parameter value sliders.
506      * @param controlPanel control panel
507      * @param controlSize control size
508      * @param strutSize space between parameters
509      */
510     private void setupParameterSliders(final JPanel controlPanel, final Dimension controlSize, final int strutSize)
511     {
512         // Tmax
513         JLabel tLabel = new JLabel("<html>Max. headway [s]</html>");
514         tLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
515         tLabel.setPreferredSize(controlSize);
516         controlPanel.add(tLabel);
517         JSlider tSlider = new JSlider(10, 20, 12);
518         Dictionary<Integer, JLabel> labels = new Hashtable<>();
519         for (int i = 10; i <= 20; i += 2)
520         {
521             labels.put(i, new JLabel(String.format("%.1f", i / 10.0)));
522         }
523         tSlider.setLabelTable(labels);
524         tSlider.setPreferredSize(controlSize);
525         tSlider.setSnapToTicks(true);
526         tSlider.setMinorTickSpacing(1);
527         tSlider.setMajorTickSpacing(2);
528         tSlider.setPaintTicks(true);
529         tSlider.setPaintLabels(true);
530         tSlider.setToolTipText("<html>Max. headway [s]</html>");
531         tSlider.addChangeListener(new ChangeListener()
532         {
533             @Override
534             public void stateChanged(final ChangeEvent e)
535             {
536                 double value = ((JSlider) e.getSource()).getValue() / 10.0;
537                 FundamentalDiagramDemo.this.tMin = Duration.ofSI((0.56 / 1.2) * value);
538                 FundamentalDiagramDemo.this.tMax = Duration.ofSI(value);
539                 FundamentalDiagramDemo.this.fdLine.update();
540                 notifyPlotsChanged();
541                 for (Gtu gtu : getNetwork().getGTUs())
542                 {
543                     try
544                     {
545                         gtu.getParameters().setClaimedParameter(ParameterTypes.TMIN, FundamentalDiagramDemo.this.tMin, tSlider);
546                         gtu.getParameters().setClaimedParameter(ParameterTypes.TMAX, FundamentalDiagramDemo.this.tMax, tSlider);
547                     }
548                     catch (ParameterException exception)
549                     {
550                         // Try different order, perhaps a large increase caused Tmin > Tmax
551                         try
552                         {
553                             gtu.getParameters().setClaimedParameter(ParameterTypes.TMAX, FundamentalDiagramDemo.this.tMax,
554                                     tSlider);
555                             gtu.getParameters().setClaimedParameter(ParameterTypes.TMIN, FundamentalDiagramDemo.this.tMin,
556                                     tSlider);
557                         }
558                         catch (ParameterException exception2)
559                         {
560                             Logger.ots().error("Unable to set headway parameter.");
561                         }
562                     }
563                 }
564             }
565         });
566         controlPanel.add(tSlider);
567         // spacer
568         controlPanel.add(Box.createVerticalStrut(strutSize));
569         // V max
570         JLabel vLabel = new JLabel("<html>Speed limit [km/h]</html>");
571         vLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
572         vLabel.setPreferredSize(controlSize);
573         controlPanel.add(vLabel);
574         JSlider vSlider = new JSlider(80, 130, 120);
575         vSlider.setPreferredSize(controlSize);
576         vSlider.setSnapToTicks(true);
577         vSlider.setMinorTickSpacing(10);
578         vSlider.setMajorTickSpacing(10);
579         vSlider.setPaintTicks(true);
580         vSlider.setPaintLabels(true);
581         vSlider.setToolTipText("<html>Speed limit [km/h]</html>");
582         vSlider.addChangeListener(new ChangeListener()
583         {
584             @Override
585             public void stateChanged(final ChangeEvent e)
586             {
587                 FundamentalDiagramDemo.this.speedLimits
588                         .addSpeedLimit(new Speed(((JSlider) e.getSource()).getValue(), SpeedUnit.KM_PER_HOUR));
589                 FundamentalDiagramDemo.this.fdLine.update();
590                 notifyPlotsChanged();
591             }
592         });
593         controlPanel.add(vSlider);
594     }
595 
596     /**
597      * Response when settings were changed that affect the shape of the theoretical fundamental diagram, i.e. the FD line.
598      */
599     void notifyPlotsChanged()
600     {
601         for (FundamentalDiagram diagram : this.funamentalDiagrams)
602         {
603             diagram.notifyPlotChange();
604         }
605     }
606 
607     /**
608      * Response to clear data button.
609      */
610     private void clearDataAndGraphs()
611     {
612         // creating a sampler (and graphs) while running gives issues with scheduling in the past (sometimes)
613         boolean wasRunning = getSimulator().isStartingOrRunning();
614         if (wasRunning)
615         {
616             getSimulator().stop();
617         }
618 
619         // new sampler to loose all data
620         if (this.sampler != null)
621         {
622             this.sampler.getSamplerData().getLanes().forEach((l) -> this.sampler.finalizeRecording((LaneDataRoad) l));
623         }
624         this.sampler = new RoadSampler(getNetwork());
625 
626         // create fundamental diagram source for each cross section (plots are (re)created in setCrossSections())
627         for (int i = 250; i <= 2250; i += 250)
628         {
629             List<String> names = new ArrayList<>();
630             names.add("Left");
631             names.add("Right");
632             Length lanePosition;
633             String linkId;
634             if (i >= 1500.0)
635             {
636                 lanePosition = Length.ofSI(i - 1500.0);
637                 linkId = "Lane-dropDestination";
638             }
639             else
640             {
641                 names.add(1, "Middle");
642                 lanePosition = Length.ofSI(i);
643                 linkId = "OriginLane-drop";
644             }
645             LinkPosition linkPosition = new LinkPosition(getNetwork().getLink(linkId).get(), lanePosition);
646             GraphCrossSection<LaneDataRoad> crossSection;
647             try
648             {
649                 crossSection = GraphLaneUtil.createCrossSection(names, linkPosition);
650             }
651             catch (NetworkException exception)
652             {
653                 throw new OtsRuntimeException("Unable to create cross section.", exception);
654             }
655             Duration aggregationTime = Duration.ofSI(30.0);
656             FdDataSource source =
657                     FdDataSource.sourceFromSampler(this.sampler, this.scheduler, crossSection, true, aggregationTime, false);
658             this.fdSourceMap.put(String.format("%.2f", i / 1000.0).replace(",", "."), source);
659         }
660 
661         // clear updates on plots that might already be there
662         if (this.trajectoryPlot != null)
663         {
664             this.scheduler.cancelEvent(this.trajectoryPlot);
665         }
666         this.funamentalDiagrams.forEach((fd) -> this.scheduler.cancelEvent(fd));
667 
668         // create sampler plot
669         List<String> names = new ArrayList<>();
670         names.add("Left lane");
671         names.add("Middle lane");
672         names.add("Right lane");
673         List<Lane> firstLanes = new ArrayList<>();
674         for (Lane lane : ((CrossSectionLink) getNetwork().getLink("OriginLane-drop").get()).getLanes())
675         {
676             firstLanes.add(lane);
677         }
678         GraphPath<LaneDataRoad> path = Try.assign(() -> GraphLaneUtil.createPath(names, firstLanes), "Missing speed limit");
679         this.trajectoryPlot =
680                 new TrajectoryPlot("Trajectories", Duration.ofSI(5.0), this.scheduler, this.sampler.getSamplerData(), path);
681         this.trajectoryPlot.updateFixedDomainRange(true);
682         SwingTrajectoryPlot swingTrajectoryPlot = new SwingTrajectoryPlot(this.trajectoryPlot)
683         {
684             /** Serialization version UID. */
685             private static final long serialVersionUID = 20251121L;
686 
687             @Override
688             protected void addPopUpMenuItems(final JPopupMenu popupMenu)
689             {
690                 // disable: always fixed time range
691             }
692         };
693         this.trajectoryPanel = swingTrajectoryPlot.getContentPane();
694 
695         // create fundamental diagrams
696         createFundamentalDiagramsForCrossSections();
697 
698         // reset simulator state
699         if (!getSimulator().isStartingOrRunning() && wasRunning)
700         {
701             getSimulator().start();
702         }
703     }
704 
705     /**
706      * Creates the fundamental diagrams based on the selected cross-sections.
707      */
708     private void createFundamentalDiagramsForCrossSections()
709     {
710         // avoid update scheduling in the past as simulator is running during creation
711         boolean wasRunning = getSimulator().isStartingOrRunning();
712         if (wasRunning)
713         {
714             getSimulator().stop();
715         }
716 
717         // keep color of selected theme in new GUI elements
718         Color color = null;
719         if (this.graphPanel != null)
720         {
721             color = this.graphPanel.getBackground();
722             // remove previous graphs
723             this.splitPanel.remove(this.graphPanel);
724         }
725         // create new panel for graphs
726         this.graphPanel = new TablePanel(2, 2);
727         this.graphPanel.setBorder(new EmptyBorder(0, 0, 20, 0));
728         if (color != null)
729         {
730             this.graphPanel.setBackground(color);
731         }
732         this.splitPanel.add(this.graphPanel);
733 
734         // compose a combined source if required
735         FdDataSource source;
736         if (this.absoluteCrossSection2.equals("None") && this.absoluteCrossSection3.equals("None"))
737         {
738             source = this.fdSourceMap.get(this.absoluteCrossSection1);
739             if (source == null)
740             {
741                 source = this.fdSourceMap.get(this.absoluteCrossSection1);
742             }
743             source.clearPlots();
744         }
745         else
746         {
747             Map<String, FdDataSource> sources = new LinkedHashMap<>();
748             sources.put(this.absoluteCrossSection1 + "km", this.fdSourceMap.get(this.absoluteCrossSection1));
749             if (!this.absoluteCrossSection2.equals("None"))
750             {
751                 sources.put(this.absoluteCrossSection2 + "km", this.fdSourceMap.get(this.absoluteCrossSection2));
752             }
753             if (!this.absoluteCrossSection3.equals("None"))
754             {
755                 sources.put(this.absoluteCrossSection3 + "km", this.fdSourceMap.get(this.absoluteCrossSection3));
756             }
757             for (FdDataSource subSource : sources.values())
758             {
759                 subSource.clearPlots();
760             }
761             source = FdDataSource.combinedSource(sources);
762         }
763 
764         // because "Aggregate" and "Theoretical" looks ugly in the legend, we set the actual location as legend label
765         source.setAggregateName(this.absoluteCrossSection1);
766 
767         // create the fundamental diagrams
768         FundamentalDiagram fdPlota =
769                 new FundamentalDiagram("Density-speed", Quantity.DENSITY, Quantity.SPEED, source, this.fdLine);
770         FundamentalDiagram fdPlotb =
771                 new FundamentalDiagram("Density-flow", Quantity.DENSITY, Quantity.FLOW, source, this.fdLine);
772         FundamentalDiagram fdPlotc = new FundamentalDiagram("Flow-speed", Quantity.FLOW, Quantity.SPEED, source, this.fdLine);
773 
774         // recalculate over past data
775         source.calculatePaintStateSafe(getSimulator().getSimulatorTime());
776 
777         // store graphs so changes to setting may affect the graphs
778         this.funamentalDiagrams.clear();
779         this.funamentalDiagrams.add(fdPlota);
780         this.funamentalDiagrams.add(fdPlotb);
781         this.funamentalDiagrams.add(fdPlotc);
782 
783         // create swing plots and add them to the graph panel
784         Container fda = new SwingFundamentalDiagram(fdPlota).getContentPane();
785         Container fdb = new SwingFundamentalDiagram(fdPlotb).getContentPane();
786         Container fdc = new SwingFundamentalDiagram(fdPlotc).getContentPane();
787         Dimension preferredGraphSize = new Dimension(375, 230);
788         fda.setPreferredSize(preferredGraphSize);
789         fdb.setPreferredSize(preferredGraphSize);
790         fdc.setPreferredSize(preferredGraphSize);
791         this.graphPanel.setCell(fda, 0, 0);
792         this.graphPanel.setCell(fdb, 0, 1);
793         this.graphPanel.setCell(fdc, 1, 0);
794 
795         // also add the trajectory panel (which hasn't changed but should be moved to the new graphs panel)
796         this.trajectoryPanel.setPreferredSize(preferredGraphSize);
797         this.graphPanel.setCell(this.trajectoryPanel, 1, 1);
798 
799         // set theme color of fundamental diagrams too (the little bar below where the mouse-over info is shown is visible)
800         if (color != null)
801         {
802             fda.setBackground(color);
803             fdb.setBackground(color);
804             fdc.setBackground(color);
805             this.trajectoryPanel.setBackground(color);
806         }
807 
808         // reorganize panels
809         fda.getParent().getParent().validate();
810 
811         // reset simulator state
812         if (!getSimulator().isStartingOrRunning() && wasRunning)
813         {
814             getSimulator().start();
815         }
816     }
817 
818     /**
819      * Fundamental diagram line class based on local settings.
820      */
821     private final class DynamicFdLine implements FdLine
822     {
823 
824         /** Map of points for each quantity. */
825         private Map<Quantity, double[]> map = new LinkedHashMap<>();
826 
827         @Override
828         public double[] getValues(final Quantity quantity)
829         {
830             return this.map.get(quantity);
831         }
832 
833         @Override
834         public String getName()
835         {
836             return "Theoretical";
837         }
838 
839         /**
840          * Recalculates the FD based on input parameters.
841          */
842         public void update()
843         {
844             // harmonic mean of desired speed of cars and trucks
845             HarmonicMean<Speed, Double> meanSpeed = new HarmonicMean<>();
846             Speed carSpeed = FundamentalDiagramDemo.this.speedLimits.getSpeedLimit(Duration.ZERO).get().speed();
847             meanSpeed.add(carSpeed, 1.0 - FundamentalDiagramDemo.this.truckFraction);
848             Speed truckSpeed = Speed.min(carSpeed, new Speed(85.0, SpeedUnit.KM_PER_HOUR));
849             meanSpeed.add(truckSpeed, FundamentalDiagramDemo.this.truckFraction);
850 
851             // mean of lengths
852             double meanLength =
853                     4.19 * (1.0 - FundamentalDiagramDemo.this.truckFraction) + 12.0 * FundamentalDiagramDemo.this.truckFraction;
854 
855             // calculate triangular FD parameters
856             double vMax = meanSpeed.getMean();
857             double kCrit = 1000.0 / (vMax * FundamentalDiagramDemo.this.tMax.si + meanLength + 3.0);
858             vMax = vMax * 3.6;
859             double qMax = vMax * kCrit;
860             int kJam = (int) (1000.0 / (meanLength + 3.0));
861 
862             // initialize and fill arrays for each quantity
863             double[] k = new double[kJam * 10 + 1];
864             double[] q = new double[kJam * 10 + 1];
865             double[] v = new double[kJam * 10 + 1];
866             for (int kk = 0; kk <= kJam * 10; kk++)
867             {
868                 double kVal = kk / 10.0;
869                 k[kk] = kVal;
870                 if (kVal > kCrit)
871                 {
872                     // congestion branch
873                     q[kk] = qMax * (1.0 - (kVal - kCrit) / (kJam - kCrit));
874                     v[kk] = q[kk] / k[kk];
875                 }
876                 else
877                 {
878                     // free-flow branch
879                     v[kk] = vMax;
880                     q[kk] = k[kk] * v[kk];
881                 }
882             }
883 
884             // cache values
885             this.map.put(Quantity.DENSITY, k);
886             this.map.put(Quantity.FLOW, q);
887             this.map.put(Quantity.SPEED, v);
888         }
889 
890     }
891 
892 }