View Javadoc
1   package nl.tudelft.simulation.dsol.swing.gui.control;
2   
3   import java.awt.Dimension;
4   import java.text.DecimalFormat;
5   import java.text.NumberFormat;
6   import java.util.Hashtable;
7   import java.util.LinkedHashMap;
8   import java.util.Map;
9   
10  import javax.swing.JLabel;
11  import javax.swing.JPanel;
12  import javax.swing.JSlider;
13  import javax.swing.SwingConstants;
14  import javax.swing.event.ChangeEvent;
15  import javax.swing.event.ChangeListener;
16  
17  import nl.tudelft.simulation.dsol.simulators.DevsRealTimeAnimator;
18  import nl.tudelft.simulation.dsol.simulators.DevsSimulatorInterface;
19  
20  /**
21   * JPanel that contains a JSider for setting the speed of the simulation using a logarithmic scale
22   * <p>
23   * Copyright (c) 2020-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
24   * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
25   * project is distributed under a three-clause BSD-style license, which can be found at
26   * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
27   * </p>
28   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
29   */
30  public class RunSpeedSliderPanel extends JPanel
31  {
32      /** */
33      private static final long serialVersionUID = 20150408L;
34  
35      /** The JSlider that the user sees. */
36      private final JSlider slider;
37  
38      /** The ratios used in each decade. */
39      private final int[] ratios;
40  
41      /** The values at each tick. */
42      private Map<Integer, Double> tickValues = new LinkedHashMap<>();
43  
44      /**
45       * Construct a new TimeWarpPanel.
46       * @param minimum double; the minimum value on the scale (the displayed scale may extend a little further than this
47       *            value)
48       * @param maximum double; the maximum value on the scale (the displayed scale may extend a little further than this
49       *            value)
50       * @param initialValue double; the initially selected value on the scale
51       * @param ticksPerDecade int; the number of steps per decade
52       * @param simulator DevsSimulatorInterface&lt;?, ?, ?&gt;; the simulator to change the speed of
53       */
54      RunSpeedSliderPanel(final double minimum, final double maximum, final double initialValue, final int ticksPerDecade,
55              final DevsSimulatorInterface<?> simulator)
56      {
57          if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
58          {
59              throw new RuntimeException("Bad (combination of) minimum, maximum and initialValue; "
60                      + "(restrictions: 0 < minimum <= initialValue <= maximum)");
61          }
62          switch (ticksPerDecade)
63          {
64              case 1:
65                  this.ratios = new int[] {1};
66                  break;
67              case 2:
68                  this.ratios = new int[] {1, 3};
69                  break;
70              case 3:
71                  this.ratios = new int[] {1, 2, 5};
72                  break;
73              default:
74                  throw new RuntimeException("Bad ticksPerDecade value (must be 1, 2 or 3)");
75          }
76          int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
77          int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
78          this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick + 1, 0);
79          this.slider.setPreferredSize(new Dimension(350, 45));
80          Hashtable<Integer, JLabel> labels = new Hashtable<>();
81          for (int step = 0; step <= maximumTick; step++)
82          {
83              StringBuilder text = new StringBuilder();
84              text.append(this.ratios[step % this.ratios.length]);
85              for (int decade = 0; decade < step / this.ratios.length; decade++)
86              {
87                  text.append("0");
88              }
89              this.tickValues.put(step, Double.parseDouble(text.toString()));
90              labels.put(step, new JLabel(text.toString().replace("000", "K")));
91              // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
92          }
93          // Figure out the DecimalSymbol
94          String decimalSeparator =
95                  "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
96          for (int step = -1; step >= minimumTick; step--)
97          {
98              StringBuilder text = new StringBuilder();
99              text.append("0");
100             text.append(decimalSeparator);
101             for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
102             {
103                 text.append("0");
104             }
105             int index = step % this.ratios.length;
106             if (index < 0)
107             {
108                 index += this.ratios.length;
109             }
110             text.append(this.ratios[index]);
111             labels.put(step, new JLabel(text.toString()));
112             this.tickValues.put(step, Double.parseDouble(text.toString()));
113             // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
114         }
115         labels.put(maximumTick + 1, new JLabel("\u221E"));
116         this.tickValues.put(maximumTick + 1, 1E9);
117         this.slider.setLabelTable(labels);
118         this.slider.setPaintLabels(true);
119         this.slider.setPaintTicks(true);
120         this.slider.setMajorTickSpacing(1);
121         this.add(this.slider);
122         /*- Uncomment to verify the stepToFactor method.
123         for (int i = this.slider.getMinimum(); i <= this.slider.getMaximum(); i++)
124         {
125             System.out.println("pos=" + i + " value is " + stepToFactor(i));
126         }
127          */
128 
129         // initial value of simulation speed
130         if (simulator instanceof DevsRealTimeAnimator)
131         {
132             DevsRealTimeAnimator<?> clock = (DevsRealTimeAnimator<?>) simulator;
133             clock.setSpeedFactor(RunSpeedSliderPanel.this.tickValues.get(this.slider.getValue()));
134         }
135 
136         // adjust the simulation speed
137         this.slider.addChangeListener(new ChangeListener()
138         {
139             @Override
140             public void stateChanged(final ChangeEvent ce)
141             {
142                 JSlider source = (JSlider) ce.getSource();
143                 if (!source.getValueIsAdjusting() && simulator instanceof DevsRealTimeAnimator)
144                 {
145                     DevsRealTimeAnimator<?> clock = (DevsRealTimeAnimator<?>) simulator;
146                     clock.setSpeedFactor(((RunSpeedSliderPanel) source.getParent()).getTickValues().get(source.getValue()));
147                 }
148             }
149         });
150     }
151 
152     /**
153      * Access to tickValues map from within the event handler.
154      * @return Map&lt;Integer, Double&gt; the tickValues map of this TimeWarpPanel
155      */
156     protected Map<Integer, Double> getTickValues()
157     {
158         return this.tickValues;
159     }
160 
161     /**
162      * Convert a position on the slider to a factor.
163      * @param step int; the position on the slider
164      * @return double; the factor that corresponds to step
165      */
166     private double stepToFactor(final int step)
167     {
168         int index = step % this.ratios.length;
169         if (index < 0)
170         {
171             index += this.ratios.length;
172         }
173         double result = this.ratios[index];
174         // Make positive to avoid trouble with negative values that round towards 0 on division
175         int power = (step + 1000 * this.ratios.length) / this.ratios.length - 1000; // This is ugly
176         while (power > 0)
177         {
178             result *= 10;
179             power--;
180         }
181         while (power < 0)
182         {
183             result /= 10;
184             power++;
185         }
186         return result;
187     }
188 
189     /**
190      * Retrieve the current TimeWarp factor.
191      * @return double; the current TimeWarp factor
192      */
193     public double getFactor()
194     {
195         return stepToFactor(this.slider.getValue());
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public String toString()
201     {
202         return "TimeWarpPanel [timeWarp=" + this.getFactor() + "]";
203     }
204 
205     /**
206      * Set the time warp factor to the best possible approximation of a given value.
207      * @param factor double; the requested speed factor
208      */
209     public void setSpeedFactor(final double factor)
210     {
211         int bestStep = -1;
212         double bestError = Double.MAX_VALUE;
213         double logOfFactor = Math.log(factor);
214         for (int step = this.slider.getMinimum(); step <= this.slider.getMaximum(); step++)
215         {
216             double ratio = getTickValues().get(step); // stepToFactor(step);
217             double logError = Math.abs(logOfFactor - Math.log(ratio));
218             if (logError < bestError)
219             {
220                 bestStep = step;
221                 bestError = logError;
222             }
223         }
224         // System.out.println("setSpeedfactor: factor is " + factor + ", best slider value is " + bestStep
225         // + " current value is " + this.slider.getValue());
226         if (this.slider.getValue() != bestStep)
227         {
228             this.slider.setValue(bestStep);
229         }
230     }
231 }