001package jmri.jmrit.roster;
002
003import java.util.ArrayList;
004import java.util.LinkedList;
005import java.util.List;
006import java.util.Locale;
007import java.util.Map.Entry;
008import java.util.TreeMap;
009
010import javax.annotation.CheckForNull;
011
012import jmri.Block;
013import jmri.DccThrottle;
014import jmri.InstanceManager;
015import jmri.NamedBean;
016import jmri.Sensor;
017import java.beans.PropertyChangeListener;
018import jmri.Section;
019import jmri.implementation.SignalSpeedMap;
020
021import org.jdom2.Element;
022
023/**
024 * A class to store a speed profile for a given loco.
025 * The speed steps against the profile are on a scale of 0 to 1000,
026 * this equates to the float speed x 1000.
027 * This allows a single profile to cover different throttle speed step settings.
028 * A profile generated for a loco using 28 steps can be used for a throttle with 126 steps.
029 */
030public class RosterSpeedProfile {
031
032    private RosterEntry _re = null;
033
034    private float overRunTimeReverse = 0.0f;
035    private float overRunTimeForward = 0.0f;
036
037    private boolean _hasForwardSpeeds = false;
038    private boolean _hasReverseSpeeds = false;
039
040    /**
041     * Create a new RosterSpeedProfile.
042     * @param re the Roster Entry associated with the profile.
043     */
044    public RosterSpeedProfile(RosterEntry re) {
045        _re = re;
046    }
047
048    /**
049     * Get the RosterEntry associated with the profile.
050     * @return the RosterEntry.
051     */
052    public RosterEntry getRosterEntry() {
053        return _re;
054    }
055
056    public float getOverRunTimeForward() {
057        return overRunTimeForward;
058    }
059
060    public void setOverRunTimeForward(float dt) {
061        overRunTimeForward = dt;
062    }
063
064    public float getOverRunTimeReverse() {
065        return overRunTimeReverse;
066    }
067
068    public void setOverRunTimeReverse(float dt) {
069        overRunTimeReverse = dt;
070    }
071
072    public void clearCurrentProfile() {
073        speeds = new TreeMap<>();
074    }
075
076    public void deleteStep(Integer step) {
077        speeds.remove(step);
078    }
079
080    /**
081     * Check if the Speed Profile contains Forward Speeds.
082     * @return true if forward speeds are present, else false.
083     */
084    public boolean hasForwardSpeeds() {
085        return _hasForwardSpeeds;
086    }
087
088    /**
089     * Check if the Speed Profile contains Reverse Speeds.
090     * @return true if reverse speeds are present, else false.
091     */
092    public boolean hasReverseSpeeds() {
093        return _hasReverseSpeeds;
094    }
095
096    /**
097     * place / remove SpeedProfile from test mode.
098     * reinitializes speedstep trace array
099     * @param value true/false
100     */
101    public void setTestMode(boolean value) {
102        synchronized (this){
103            profileInTestMode = value;
104        }
105        testSteps = new ArrayList<>();
106    }
107
108    /**
109     * Gets the speed step trace array.
110     * @return speedstep trace array
111     */
112    public List<SpeedSetting> getSpeedStepTrace() {
113        return testSteps;
114    }
115
116    /**
117     * Speed conversion Millimetres per second to Miles per hour.
118     */
119    public static final float MMS_TO_MPH = 0.00223694f;
120
121    /**
122     * Speed conversion Millimetres per second to Kilometres per hour.
123     */
124    public static final float MMS_TO_KPH = 0.0036f;
125
126    /**
127     * Returns the scale speed.
128     * If Warrant preferences are not a speed, value returns unchanged.
129     * @param mms MilliMetres per second.
130     * @param factorFastClock true to factor in the Fast Clock ratio, else false.
131     * @return scale speed in units specified by Warrant Preferences,
132     *         unchanged if Warrant preferences are not a speed.
133     */
134    public float mmsToScaleSpeed(float mms, boolean factorFastClock) {
135        int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation();
136        float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale();
137        float fastClockFactor = ( factorFastClock ?
138            (float)InstanceManager.getDefault(jmri.Timebase.class).userGetRate() : 1 );
139
140        switch (interp) {
141            case SignalSpeedMap.SPEED_MPH:
142                return mms * scale * MMS_TO_MPH * fastClockFactor;
143            case SignalSpeedMap.SPEED_KMPH:
144                return mms * scale * MMS_TO_KPH * fastClockFactor;
145            case SignalSpeedMap.PERCENT_THROTTLE:
146            case SignalSpeedMap.PERCENT_NORMAL:
147                return mms;
148            default:
149                log.warn("MMSToScaleSpeed: Signal Speed Map is not in a scale speed, not modifing.");
150                return mms;
151        }
152    }
153
154    /**
155     * Returns the scale speed as a numeric.
156     * If Warrant preferences are not a speed, value returns unchanged.
157     * @param mms MilliMetres per second
158     * @return scale speed in units specified by Warrant Preferences,
159     *         unchanged if Warrant preferences are not a speed.
160     * @deprecated use {@link #mmsToScaleSpeed(float mms)}
161     */
162    @Deprecated (since="5.9.6",forRemoval=true)
163    public float MMSToScaleSpeed(float mms) {
164        jmri.util.LoggingUtil.deprecationWarning(log, "MMSToScaleSpeed");
165        return mmsToScaleSpeed(mms);
166    }
167
168    /**
169     * Returns the scale speed as a numeric.
170     * If Warrant preferences are not a speed, value returns unchanged.
171     * Does not factor Fast Clock ratio.
172     * @param mms MilliMetres per second
173     * @return scale speed in units specified by Warrant Preferences,
174     *         unchanged if Warrant preferences are not a speed.
175     */
176    public float mmsToScaleSpeed(float mms) {
177        return mmsToScaleSpeed(mms, false);
178    }
179
180    /**
181     * Returns the scale speed format as I18N string with the units added given
182     * MilliMetres per Second.
183     * If the warrant preference is a percentage of
184     * normal or throttle will use metres per second.
185     * The Fast Clock Ratio is not used in the calculation.
186     *
187     * @param mms MilliMetres per second
188     * @return a string with scale speed and units
189     */
190    public static String convertMMSToScaleSpeedWithUnits(float mms) {
191        int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation();
192        float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale();
193        String formattedWithUnits;
194        switch (interp) {
195            case SignalSpeedMap.SPEED_MPH:
196                String unitsMph = Bundle.getMessage("mph");
197                formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms * scale * MMS_TO_MPH, unitsMph);
198                break;
199            case SignalSpeedMap.SPEED_KMPH:
200                String unitsKph = Bundle.getMessage("kph");
201                formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms * scale * MMS_TO_KPH, unitsKph);
202                break;
203            case SignalSpeedMap.PERCENT_THROTTLE:
204            case SignalSpeedMap.PERCENT_NORMAL:
205                String unitsMms = Bundle.getMessage("mmps");
206                formattedWithUnits = String.format(Locale.getDefault(), "%.2f %s", mms, unitsMms);
207                break;
208            default:
209                log.warn("ScaleSpeedToMMS: Signal Speed Map has no interp, not modifing.");
210                formattedWithUnits = String.format( Locale.getDefault(), "%.2f", mms);
211        }
212        return formattedWithUnits;
213    }
214
215    /**
216     * Returns the scale speed format as a string with the units added given a
217     * throttle setting. and direction.
218     * The Fast Clock Ratio is not used in the calculation.
219     *
220     * @param throttleSetting as percentage of 1.0
221     * @param isForward       true or false
222     * @return a string with scale speed and units
223     */
224    public String convertThrottleSettingToScaleSpeedWithUnits(float throttleSetting, boolean isForward) {
225        return convertMMSToScaleSpeedWithUnits(getSpeed(throttleSetting, isForward));
226    }
227
228    /**
229     * MilliMetres per Second given scale speed.
230     * The Fast Clock Ratio is not used in the calculation.
231     * @param scaleSpeed in MPH or KPH
232     * @return MilliMetres per second
233     */
234    public float convertScaleSpeedToMMS(float scaleSpeed) {
235        int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation();
236        float scale = InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale();
237        float mmsSpeed;
238        switch (interp) {
239            case SignalSpeedMap.SPEED_MPH:
240                mmsSpeed = scaleSpeed / scale / MMS_TO_MPH;
241                break;
242            case SignalSpeedMap.SPEED_KMPH:
243                mmsSpeed = scaleSpeed / scale / MMS_TO_KPH;
244                break;
245            default:
246                log.warn("ScaleSpeedToMMS: Signal Speed Map is not in a scale speed, not modifing.");
247                mmsSpeed = scaleSpeed;
248        }
249        return mmsSpeed;
250    }
251
252    /**
253     * Converts from signal map speed to a throttle setting.
254     * The Fast Clock Ratio is not used in the calculation.
255     * @param signalMapSpeed value from warrants preferences
256     * @param isForward      direction of travel
257     * @return throttle setting
258     */
259    public float getThrottleSettingFromSignalMapSpeed(float signalMapSpeed, boolean isForward) {
260        int interp = InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation();
261        float throttleSetting = 0.0f;
262        switch (interp) {
263            case SignalSpeedMap.PERCENT_NORMAL:
264            case SignalSpeedMap.PERCENT_THROTTLE:
265                throttleSetting = signalMapSpeed / 100.0f;
266                break;
267            case SignalSpeedMap.SPEED_KMPH:
268            case SignalSpeedMap.SPEED_MPH:
269                throttleSetting = getThrottleSetting(convertScaleSpeedToMMS(signalMapSpeed), isForward);
270                break;
271            default:
272                log.warn("getThrottleSettingFromSignalMapSpeed: Signal Speed Map interp not supported.");
273        }
274        return throttleSetting;
275    }
276
277    /**
278     * Set the speed for the given speed step.
279     *
280     * @param speedStep the speed step to set
281     * @param forward   speed in meters per second for running forward at
282     *                  speedStep
283     * @param reverse   speed in meters per second for running in reverse at
284     *                  speedStep
285     */
286    public void setSpeed(int speedStep, float forward, float reverse) {
287        SpeedStep ss = speeds.computeIfAbsent(speedStep, k -> new SpeedStep());
288        ss.setForwardSpeed(forward);
289        ss.setReverseSpeed(reverse);
290        if (forward > 0.0f) {
291            _hasForwardSpeeds = true;
292        }
293        if (reverse > 0.0f) {
294            _hasReverseSpeeds = true;
295        }
296    }
297
298    public SpeedStep getSpeedStep(float speed) {
299        int iSpeedStep = Math.round(speed * 1000);
300        return speeds.get(iSpeedStep);
301    }
302
303    public void setForwardSpeed(float speedStep, float forward) {
304        if (forward > 0.0f) {
305            _hasForwardSpeeds = true;
306        } else {
307            return;
308        }
309        int iSpeedStep = Math.round(speedStep * 1000);
310        speeds.computeIfAbsent(iSpeedStep, k -> new SpeedStep()).setForwardSpeed(forward);
311    }
312
313    /**
314     * Merge raw throttleSetting value with an existing profile SpeedStep if
315     * key for the throttleSetting is within the speedIncrement of the SpeedStep.
316     * @param throttleSetting raw throttle setting value
317     * @param speed track speed
318     * @param speedIncrement throttle's speed step increment.
319     */
320    public void setForwardSpeed(float throttleSetting, float speed, float speedIncrement) {
321        if (throttleSetting> 0.0f) {
322            _hasForwardSpeeds = true;
323        } else {
324            return;
325        }
326        int key;
327        Entry<Integer, SpeedStep> entry = findEquivalentEntry (throttleSetting, speedIncrement);
328        if (entry != null) {    // close keys. i.e. resolve to same throttle step
329            float value = entry.getValue().getForwardSpeed();
330            speed = (speed + value) / 2;
331            key = entry.getKey();
332        } else {    // nothing close. make new entry
333            key = Math.round(throttleSetting * 1000);
334        }
335        speeds.computeIfAbsent(key, k -> new SpeedStep()).setForwardSpeed(speed);
336    }
337
338    @CheckForNull
339    private Entry<Integer, SpeedStep> findEquivalentEntry (float throttleSetting, float speedIncrement) {
340        // search through table until end for an entry is found whose key / 1000
341        // is within the speedIncrement of the throttleSetting
342        // Note there may be zero values interspersed in the tree
343        Entry<Integer, SpeedStep> entry = speeds.firstEntry();
344        if (entry == null) {
345            return null;
346        }
347        int key = entry.getKey();
348        while (entry != null) {
349            entry = speeds.higherEntry(key);
350            if (entry != null) {
351                float speed = entry.getKey();
352                if (Math.abs(speed/1000.0f - throttleSetting) <= speedIncrement) {
353                    return entry;
354                }
355                key = entry.getKey();
356            }
357        }
358        return null;
359    }
360
361    /**
362     * Merge raw throttleSetting value with an existing profile SpeedStep if
363     * key for the throttleSetting is within the speedIncrement of the SpeedStep.
364     * @param throttleSetting raw throttle setting value
365     * @param speed track speed
366     * @param speedIncrement throttle's speed step increment.
367     */
368    public void setReverseSpeed(float throttleSetting, float speed, float speedIncrement) {
369        if (throttleSetting> 0.0f) {
370            _hasReverseSpeeds = true;
371        } else {
372            return;
373        }
374        int key;
375        Entry<Integer, SpeedStep> entry = findEquivalentEntry (throttleSetting, speedIncrement);
376        if (entry != null) {    // close keys. i.e. resolve to same throttle step
377            float value = entry.getValue().getReverseSpeed();
378            speed = (speed + value) / 2;
379            key = entry.getKey();
380        } else {    // nothing close. make new entry
381            key = Math.round(throttleSetting * 1000);
382        }
383        speeds.computeIfAbsent(key, k -> new SpeedStep()).setReverseSpeed(speed);
384    }
385
386    public void setReverseSpeed(float speedStep, float reverse) {
387        if (reverse > 0.0f) {
388            _hasReverseSpeeds = true;
389        } else {
390            return;
391        }
392        int iSpeedStep = Math.round(speedStep * 1000);
393        speeds.computeIfAbsent(iSpeedStep, k -> new SpeedStep()).setReverseSpeed(reverse);
394    }
395
396    /**
397     * return the forward speed in milli-meters per second for a given
398     * percentage throttle
399     *
400     * @param speedStep which is actual percentage throttle
401     * @return MilliMetres per second using straight line interpolation for
402     *         missing points
403     */
404    public float getForwardSpeed(float speedStep) {
405        int iSpeedStep = Math.round(speedStep * 1000);
406        if (iSpeedStep <= 0 || !_hasForwardSpeeds) {
407            return 0.0f;
408        }
409        // Note there may be zero values interspersed in the tree
410        if (speeds.containsKey(iSpeedStep)) {
411            float speed = speeds.get(iSpeedStep).getForwardSpeed();
412            if (speed > 0.0f) {
413                return speed;
414            }
415        }
416        log.trace("no exact match forward for {}", iSpeedStep);
417        float lower = 0.0f;
418        float higher = 0.0f;
419        int highStep = iSpeedStep;
420        int lowStep = iSpeedStep;
421
422        Entry<Integer, SpeedStep> entry = speeds.higherEntry(highStep);
423        while (entry != null && higher <= 0.0f) {
424            highStep = entry.getKey();
425            float value = entry.getValue().getForwardSpeed();
426            if (value > 0.0f) {
427                higher = value;
428            }
429            entry = speeds.higherEntry(highStep);
430        }
431        boolean nothingHigher = (higher <= 0.0f);
432
433        entry = speeds.lowerEntry(lowStep);
434        while (entry != null && lower <= 0.0f) {
435            lowStep = entry.getKey();
436            float value = entry.getValue().getForwardSpeed();
437            if (value > 0.0f) {
438                lower = value;
439            }
440            entry = speeds.lowerEntry(lowStep);
441        }
442        log.trace("lowStep={}, lower={} highStep={} higher={} for iSpeedStep={}",
443                lowStep, lower, highStep, higher, iSpeedStep);
444        if (lower <= 0.0f) {      // nothing lower
445            if (nothingHigher) {
446                log.error("Nothing in speed Profile");
447                return 0.0f;       // no forward speeds at all
448            }
449            return higher * iSpeedStep / highStep;
450        }
451        if (nothingHigher) {
452//            return lower * (1.0f + (iSpeedStep - lowStep) / (1000.0f - lowStep));
453            return lower + (iSpeedStep - lowStep) * lower / lowStep;
454        }
455
456        float valperstep = (higher - lower) / (highStep - lowStep);
457
458        return lower + (valperstep * (iSpeedStep - lowStep));
459    }
460
461    /**
462     * return the reverse speed in millimetres per second for a given percentage
463     * throttle
464     *
465     * @param speedStep percentage of throttle 0.nnn
466     * @return millimetres per second
467     */
468    public float getReverseSpeed(float speedStep) {
469        int iSpeedStep = Math.round(speedStep * 1000);
470        if (iSpeedStep <= 0 || !_hasReverseSpeeds) {
471            return 0.0f;
472        }
473        if (speeds.containsKey(iSpeedStep)) {
474            float speed = speeds.get(iSpeedStep).getReverseSpeed();
475            if (speed > 0.0f) {
476                return speed;
477            }
478        }
479        log.trace("no exact match reverse for {}", iSpeedStep);
480        float lower = 0.0f;
481        float higher = 0.0f;
482        int highStep = iSpeedStep;
483        int lowStep = iSpeedStep;
484        // Note there may be zero values interspersed in the tree
485
486        Entry<Integer, SpeedStep> entry = speeds.higherEntry(highStep);
487        while (entry != null && higher <= 0.0f) {
488            highStep = entry.getKey();
489            float value = entry.getValue().getReverseSpeed();
490            if (value > 0.0f) {
491                higher = value;
492            }
493            entry = speeds.higherEntry(highStep);
494        }
495        boolean nothingHigher = (higher <= 0.0f);
496        entry = speeds.lowerEntry(lowStep);
497        while (entry != null && lower <= 0.0f) {
498            lowStep = entry.getKey();
499            float value = entry.getValue().getReverseSpeed();
500            if (value > 0.0f) {
501                lower = value;
502            }
503            entry = speeds.lowerEntry(lowStep);
504        }
505        log.trace("lowStep={}, lower={} highStep={} higher={} for iSpeedStep={}",
506                lowStep, lower, highStep, higher, iSpeedStep);
507        if (lower <= 0.0f) {      // nothing lower
508            if (nothingHigher) {
509                log.error("Nothing in speed Profile");
510                return 0.0f;       // no reverse speeds at all
511            }
512            return higher * iSpeedStep / highStep;
513        }
514        if (nothingHigher) {
515            return lower * (1.0f + (iSpeedStep - lowStep) / (1000.0f - lowStep));
516        }
517
518        float valperstep = (higher - lower) / (highStep - lowStep);
519
520        return lower + (valperstep * (iSpeedStep - lowStep));
521    }
522
523    /**
524     * Get the approximate time a loco may travel a given distance at a given
525     * speed step.
526     *
527     * @param isForward true if loco is running forward; false otherwise
528     * @param speedStep the desired speed step
529     * @param distance  the desired distance in millimeters
530     * @return the approximate time in seconds
531     */
532    public float getDurationOfTravelInSeconds(boolean isForward, float speedStep, int distance) {
533        float spd;
534        if (isForward) {
535            spd = getForwardSpeed(speedStep);
536        } else {
537            spd = getReverseSpeed(speedStep);
538        }
539        if (spd < 0.0f) {
540            log.error("Speed not available to compute duration of travel");
541            return 0.0f;
542        }
543        return (distance / spd);
544    }
545
546    /**
547     * Get the approximate distance a loco may travel a given duration at a
548     * given speed step.
549     *
550     * @param isForward true if loco is running forward; false otherwise
551     * @param speedStep the desired speed step
552     * @param duration  the desired time in seconds
553     * @return the approximate distance in millimeters
554     */
555    public float getDistanceTravelled(boolean isForward, float speedStep, float duration) {
556        float spd;
557        if (isForward) {
558            spd = getForwardSpeed(speedStep);
559        } else {
560            spd = getReverseSpeed(speedStep);
561        }
562        if (spd < 0.0f) {
563            log.error("Speed not available to compute distance travelled");
564            return 0.0f;
565        }
566        return Math.abs(spd * duration);
567    }
568
569    /*
570     * ============================================================
571     * Distance-based stopping API (public) - Stop to zero over a given distance
572     * (mm) - Approach to min reliable speed over a distance, then stop at
573     * sensor Notes: - Executes via RosterSpeedProfile's own stepQueue/stopTimer
574     * (H2/4A). - Overrun compensation ONLY for stop-to-zero (H4). - Optional
575     * speedFactor pre-divide supported (H3 / 4B).
576     * ============================================================
577     */
578
579    /**
580     * Plan and execute a stop-to-zero over a given distance (actual
581     * millimetres).
582     * 
583     * @param t          The DccThrottle to drive
584     * @param distanceMm Distance in mm ({@code >=} 0)
585     */
586    public void planStopToZeroOverDistance(DccThrottle t, float distanceMm) {
587        planStopToZeroOverDistance(t, distanceMm, /* speedFactor */ 1.0f);
588    }
589
590    /**
591     * Plan and execute a stop-to-zero over a given distance (actual
592     * millimetres), with an optional external speed factor pre-divide (see
593     * AutoActiveTrain behaviour).
594     * 
595     * @param t           The DccThrottle to drive
596     * @param distanceMm  Distance in mm ({@code >=} 0)
597     * @param speedFactor If {@code >} 0, throttle commands are divided by this
598     *                    factor before enqueuing.
599     */
600    public void planStopToZeroOverDistance(DccThrottle t, float distanceMm, float speedFactor) {
601        planDistanceSchedule(t, distanceMm, /* toMinOnly */ false, speedFactor);
602    }
603
604    /**
605     * Plan and execute an approach to the minimum reliable operating speed over
606     * the given distance, then stop when the supplied sensor transitions to
607     * ACTIVE.
608     * 
609     * @param t          The DccThrottle to drive
610     * @param distanceMm Distance in mm ({@code >=} 0)
611     * @param stopSensor The sensor on which to stop (must not be null)
612     */
613    public void planApproachToMinOverDistanceThenStopBySensor(
614            DccThrottle t, float distanceMm, jmri.Sensor stopSensor) {
615        planApproachToMinOverDistanceThenStopBySensor(t, distanceMm, stopSensor, /*
616                                                                                  * speedFactor
617                                                                                  */ 1.0f);
618    }
619
620    /**
621     * Plan and execute an approach to the minimum reliable operating speed over
622     * the given distance, then stop when the supplied sensor transitions to
623     * ACTIVE. Supports optional speed factor pre-divide.
624     * 
625     * @param t           The DccThrottle to drive
626     * @param distanceMm  Distance in mm ({@code >=} 0)
627     * @param stopSensor  The sensor on which to stop (must not be null)
628     * @param speedFactor If {@code >} 0, throttle commands are divided by this
629     *                    factor before enqueuing.
630     */
631    public void planApproachToMinOverDistanceThenStopBySensor(
632            DccThrottle t, float distanceMm, jmri.Sensor stopSensor, float speedFactor) {
633        if (stopSensor == null) {
634            log.warn("planApproachToMin... called with null stopSensor; forcing immediate stop.");
635            if (t != null)
636                t.setSpeedSetting(0.0f);
637            return;
638        }
639        // Stash the sensor + a one-shot listener; finishChange() removes any leftover listener.
640        approachStopSensor = stopSensor;
641        approachStopSensorListener = (java.beans.PropertyChangeEvent e) -> {
642            if ("KnownState".equals(e.getPropertyName())) {
643                try {
644                    if (((Integer) e.getNewValue()).intValue() == jmri.Sensor.ACTIVE) {
645                        if (_throttle != null) {
646                            lastIssuedSpeedSetting = 0.0f;
647                            _throttle.setSpeedSetting(0.0f);
648                        }
649                        finishChange(); // also detaches this listener
650                    }
651                } catch (RuntimeException ex) {
652                    log.warn("Stop-by-sensor handler failed; forcing stop.", ex);
653                    try {
654                        if (_throttle != null)
655                            _throttle.setSpeedSetting(0.0f);
656                    } catch (Exception ex2) {
657                        log.debug("Stop-by-sensor handler could not force stop.", ex2);
658                    }
659                    finishChange();
660                }
661            }
662        };
663        approachStopSensor.addPropertyChangeListener(approachStopSensorListener);
664
665        planDistanceSchedule(t, distanceMm, /* toMinOnly */ true, speedFactor);
666    }
667
668    /*
669     * ============================================================ Helpers for
670     * distance planning (mirrors inner controller logic)
671     * ============================================================
672     */
673
674    /** Clamp helper for throttle percentage. */
675    private static float clampPct(float pct) {
676        if (pct < 0.0f)
677            return 0.0f;
678        if (pct > 1.0f)
679            return 1.0f;
680        return pct;
681    }
682
683    /**
684     * Invert the roster profile: map target speed (mm/s) -> throttle % via
685     * bisection.
686     */
687    private float throttleForSpeedMms(final float targetMms, final boolean forward,
688            final float minPct, final float maxPct) {
689        float lo = clampPct(minPct);
690        float hi = clampPct(maxPct);
691        // Guard: if target is below/above bracket, return bracket end
692        float loMms = getSpeed(lo, forward);
693        float hiMms = getSpeed(hi, forward);
694        if (targetMms <= loMms)
695            return lo;
696        if (targetMms >= hiMms)
697            return hi;
698
699        float x = 0.5f * (lo + hi);
700        for (int i = 0; i < 18; i++) { // ~0.004 resolution
701            x = 0.5f * (lo + hi);
702            float xmms = getSpeed(x, forward);
703            if (xmms < targetMms)
704                lo = x;
705            else
706                hi = x;
707        }
708        return clampPct(x);
709    }
710
711    /**
712     * Core planner: builds a constant-deceleration throttle schedule to reach
713     * either: - zero speed exactly at distance (toMinOnly=false), applying
714     * overrun compensation; or - minimum reliable operating speed at distance
715     * (toMinOnly=true), then waits for stopSensor. Executes via this profile's
716     * own stepQueue/stopTimer (H2/4A).
717     */
718    private void planDistanceSchedule(DccThrottle t, float distanceMm, boolean toMinOnly, float speedFactor) {
719        if (t == null) {
720            log.warn("planDistanceSchedule called with null throttle; ignoring.");
721            return;
722        }
723        // Do not clobber caller-configured min/max limits; just read them
724        final float minPct = this.minReliableOperatingSpeed; // 0..1
725        final float maxPct = this.maxOperatingSpeed; // 0..1
726        final boolean forward = t.getIsForward();
727
728        // Kill any running timer WITHOUT resetting limits (avoid finishChange() here).
729        if (stopTimer != null) {
730            stopTimer.stop();
731            stopTimer = null;
732        }
733        synchronized (this) {
734            stepQueue = new LinkedList<>();
735        }
736
737        _throttle = t;
738        // Seed the "effective current" with a quantized value to avoid relying on getSpeedSetting() semantics.
739        lastIssuedSpeedSetting = quantizeToSpeedStep(_throttle, clampPct(_throttle.getSpeedSetting()));
740
741        // Apply a safe speedFactor
742        float speedFactorSafe = (speedFactor > 0.0f) ? speedFactor : 1.0f;
743
744        if (distanceMm <= 0.0f) {
745            if (toMinOnly) {
746                // Assert crawl and return; sensor listener (if any) will stop us.
747                float vMin = getSpeed(Math.max(0.0f, minPct), forward);
748                float thrMin = throttleForSpeedMms(vMin, forward, minPct, maxPct);
749                thrMin = clampPct(thrMin / speedFactorSafe);
750                thrMin = quantizeToSpeedStep(_throttle, thrMin);
751                lastIssuedSpeedSetting = thrMin;
752                _throttle.setSpeedSetting(thrMin);
753                return;
754            } else {
755                lastIssuedSpeedSetting = 0.0f;
756                _throttle.setSpeedSetting(0.0f);
757                return;
758        }
759        }
760
761        // Current speed (mm/s) from quantized speed setting
762        float thrNow = lastIssuedSpeedSetting;
763        float v0 = getSpeed(thrNow, forward);
764        float vMin = getSpeed(Math.max(0.0f, minPct), forward);
765        float vMax = getSpeed(Math.min(1.0f, maxPct), forward);
766
767        // If caller asked for approach-to-min but the configured minimum is effectively zero,
768        // then "approach to min" equals "stop to zero".
769        if (toMinOnly && vMin <= 0.0f) {
770            log.warn("planDistanceSchedule: minReliableOperatingSpeed=0; falling back to stop-to-zero over distance");
771            toMinOnly = false;
772        }
773
774        // Clamp v0 into [vMin, vMax] to reflect realistic low/high bounds
775        if (v0 < vMin)
776            v0 = vMin;
777        if (v0 > vMax)
778            v0 = vMax;
779
780        // Adjust target distance for overrun ONLY for stop-to-zero.
781        float s = distanceMm;
782        if (!toMinOnly) {
783            float overrunSec = forward ? getOverRunTimeForward() : getOverRunTimeReverse();
784            if (overrunSec < 0.0f)
785                overrunSec = 0.0f;
786            s = s - (vMin * overrunSec);
787            if (s < Math.max(0.0f, 0.5f * vMin)) {
788                s = Math.max(0.0f, 0.5f * vMin);
789        }
790        }
791
792        // If no distance effectively remains, set terminal target right away
793        if (s <= 0.0f) {
794            if (toMinOnly) {
795                float thrMin = throttleForSpeedMms(vMin, forward, minPct, maxPct);
796                thrMin = clampPct(thrMin / speedFactorSafe);
797                thrMin = quantizeToSpeedStep(_throttle, thrMin);
798                lastIssuedSpeedSetting = thrMin;
799                _throttle.setSpeedSetting(thrMin);
800            } else {
801                lastIssuedSpeedSetting = 0.0f;
802                _throttle.setSpeedSetting(0.0f);
803        }
804            return;
805        }
806
807        // Constant deceleration to meet distance at v=0 (or to vMin then hold).
808        final int internalSliceMs = 50; // internal integration resolution
809        final float dt = internalSliceMs / 1000.0f;
810        final int minCmdMs = getEffectiveMinCommandIntervalMs();
811
812        float a; // mm/s^2
813        if (!toMinOnly) {
814            a = (v0 > 0.0f) ? -(v0 * v0) / (2.0f * s) : 0.0f;
815        } else {
816            a = (v0 > vMin && s > 0.0f) ? -((v0 * v0) - (vMin * vMin)) / (2.0f * s) : 0.0f;
817        }
818
819        java.util.LinkedList<SpeedSetting> plan = new java.util.LinkedList<>();
820        float travelled = 0.0f;
821        float v = v0;
822
823        // Bucket accumulator to enforce command rate limiting without materially changing the integrated distance.
824        int bucketMs = 0;
825        float bucketSpeedTime = 0.0f; // sum of (mms * seconds)
826
827        while (travelled < s) {
828            float remaining = s - travelled;
829            float stepDt = dt;
830            if (toMinOnly && v > vMin && a != 0.0f) {
831                float tToMin = (v - vMin) / Math.abs(a);
832                if (tToMin > 0.0f && tToMin < stepDt)
833                    stepDt = tToMin;
834            }
835
836            // Predict next speed
837            float vNext;
838            if (!toMinOnly) {
839                vNext = Math.max(0.0f, v + a * stepDt);
840            } else {
841                float raw = v + a * stepDt;
842                vNext = (raw >= vMin) ? raw : vMin;
843            }
844
845            float vStart = v;
846            float vEnd = vNext;
847            if (toMinOnly) {
848                if (vStart < vMin)
849                    vStart = vMin;
850                if (vEnd < vMin)
851                    vEnd = vMin;
852            }
853            float vMid = 0.5f * (vStart + vEnd);
854            if (vMid < 0.0f)
855                vMid = 0.0f;
856
857            // Distance in this slice
858            float deltaS;
859            if (!toMinOnly) {
860                deltaS = v * stepDt + 0.5f * a * stepDt * stepDt;
861            } else if (a != 0.0f && v > vMin && vNext >= vMin) {
862                deltaS = v * stepDt + 0.5f * a * stepDt * stepDt;
863            } else {
864                deltaS = vMin * stepDt;
865            }
866            if (deltaS < 0.0f)
867                deltaS = 0.0f;
868
869            // If this slice would overshoot, shorten to land exactly.
870            if (deltaS > remaining && vMid > 0.0f) {
871                float dtFinal = remaining / vMid;
872                if (dtFinal < 0.001f)
873                    dtFinal = 0.001f;
874                int msFinal = Math.max(1, Math.round(dtFinal * 1000.0f));
875                bucketMs += msFinal;
876                bucketSpeedTime += vMid * (msFinal / 1000.0f);
877
878                // Flush the bucket at end of run.
879                float bucketSec = bucketMs / 1000.0f;
880                float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f;
881                float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct);
882                thr = clampPct(thr / speedFactorSafe);
883                thr = quantizeToSpeedStep(_throttle, thr);
884                plan.add(new SpeedSetting(thr, bucketMs, false));
885                bucketMs = 0;
886                bucketSpeedTime = 0.0f;
887                break;
888            }
889
890            int ms = Math.max(1, Math.round(stepDt * 1000.0f));
891            bucketMs += ms;
892            bucketSpeedTime += vMid * (ms / 1000.0f);
893
894            travelled += deltaS;
895            v = vNext;
896
897            // Flush the bucket when we reach the minimum command interval or at the end.
898            if (bucketMs >= minCmdMs || travelled >= s) {
899                float bucketSec = bucketMs / 1000.0f;
900                float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f;
901                float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct);
902                thr = clampPct(thr / speedFactorSafe);
903                thr = quantizeToSpeedStep(_throttle, thr);
904                plan.add(new SpeedSetting(thr, bucketMs, false));
905                bucketMs = 0;
906                bucketSpeedTime = 0.0f;
907        }
908
909            if (!toMinOnly && v <= 0.0f && travelled < s) {
910                break;
911        }
912        }
913
914        // Tail: for stop-to-zero, ensure a final explicit zero command.
915        if (!toMinOnly) {
916            int tailMs = Math.max(minCmdMs, internalSliceMs);
917            plan.add(new SpeedSetting(0.0f, tailMs, false));
918        }
919
920        // Enqueue and kick timer
921        synchronized (this) {
922            for (SpeedSetting ss : plan) {
923                stepQueue.addLast(ss);
924                if (profileInTestMode)
925                    testSteps.add(ss);
926        }
927    }
928    if (stopTimer == null) {
929        setNextStep();
930    }
931}
932
933    private float distanceRemaining = 0;
934    private float distanceTravelled = 0;
935
936    private TreeMap<Integer, SpeedStep> speeds = new TreeMap<>();
937
938    private DccThrottle _throttle;
939
940    private float desiredSpeedStep = -1;
941
942    private float extraDelay = 0.0f;
943
944    private float minReliableOperatingSpeed = 0.0f;
945
946    private float maxOperatingSpeed = 1.0f;
947
948    private NamedBean referenced = null;
949    private javax.swing.Timer stopTimer = null;
950
951    // --- Throttle command pacing / quantization ---
952    // Default minimum time between speed-setting commands issued by the distance/physics planners.
953    // The maintainers have found that sending more frequently than ~2/sec can become inaccurate
954    // due to delays through the chain and other concurrent traffic.
955    private static final int DEFAULT_MIN_COMMAND_INTERVAL_MS = 500;
956    private int minCommandIntervalMs = DEFAULT_MIN_COMMAND_INTERVAL_MS;
957
958    // Track the last speed-setting value actually issued by this class (after quantization).
959    // Do not rely on throttle.getSpeedSetting() to reflect what was finally sent to track.
960    private float lastIssuedSpeedSetting = -1.0f;
961
962    /**
963     * Set the minimum command interval (ms) used by the distance/physics
964     * planners. Values {@code <=} 0 revert to the default. Values less than
965     * DEFAULT_MIN_COMMAND_INTERVAL_MS are clamped up to the default.
966     *
967     * @param ms Minimum interval in milliseconds.
968     */
969    public void setMinCommandIntervalMs(int ms) {
970        if (ms <= 0) {
971            minCommandIntervalMs = DEFAULT_MIN_COMMAND_INTERVAL_MS;
972        } else {
973            minCommandIntervalMs = Math.max(ms, DEFAULT_MIN_COMMAND_INTERVAL_MS);
974        }
975    }
976
977    private int getEffectiveMinCommandIntervalMs() {
978        return Math.max(minCommandIntervalMs, DEFAULT_MIN_COMMAND_INTERVAL_MS);
979    }
980
981    private static float quantizeToSpeedStep(DccThrottle t, float pct) {
982        float v = clampPct(pct);
983        if (t == null)
984            return v;
985        float inc;
986        try {
987            inc = t.getSpeedIncrement();
988        } catch (Throwable ex) {
989            inc = 0.0f;
990        }
991        if (inc <= 0.0f)
992            return v;
993        // Round to nearest speed step.
994        int steps = Math.round(v / inc);
995        float q = steps * inc;
996        // Ensure any non-zero request is at least one step.
997        if (v > 0.0f && q < inc)
998            q = inc;
999        return clampPct(q);
1000    }
1001
1002    private float getEffectiveCurrentSpeedSetting() {
1003        if (lastIssuedSpeedSetting >= 0.0f) {
1004            return lastIssuedSpeedSetting;
1005        }
1006        if (_throttle != null) {
1007            return clampPct(_throttle.getSpeedSetting());
1008        }
1009        return 0.0f;
1010    }
1011
1012    // Distance-based approach-to-min: optional stop-sensor hook (cleared in finishChange()).
1013    private Sensor approachStopSensor = null;
1014    private PropertyChangeListener approachStopSensorListener = null;
1015
1016    private long lastTimeTimerStarted = 0L;
1017
1018    /**
1019     * reset everything back to default once the change has finished.
1020     */
1021    void finishChange() {
1022        // Remove any approach-stop sensor listener if present.
1023        if (approachStopSensor != null && approachStopSensorListener != null) {
1024            try {
1025                approachStopSensor.removePropertyChangeListener(approachStopSensorListener);
1026            } catch (Exception ex) {
1027                log.debug("finishChange: failed to remove approach stop sensor listener", ex);
1028            }
1029        }
1030        approachStopSensor = null;
1031        approachStopSensorListener = null;
1032        if (stopTimer != null) {
1033            stopTimer.stop();
1034        }
1035        stopTimer = null;
1036        _throttle = null;
1037        distanceRemaining = 0;
1038        desiredSpeedStep = -1;
1039        extraDelay = 0.0f;
1040        minReliableOperatingSpeed = 0.0f;
1041        maxOperatingSpeed = 1.0f;
1042        referenced = null;
1043        lastIssuedSpeedSetting = -1.0f;
1044        synchronized (this) {
1045            distanceTravelled = 0;
1046            stepQueue = new LinkedList<>();
1047        }
1048        _throttle = null;
1049    }
1050
1051    public void setExtraInitialDelay(float eDelay) {
1052        extraDelay = eDelay;
1053    }
1054
1055    public void setMinMaxLimits(float minReliableOperatingSpeed, float maxOperatingSpeed) {
1056        this.minReliableOperatingSpeed = minReliableOperatingSpeed;
1057        this.maxOperatingSpeed = maxOperatingSpeed;
1058        if (minReliableOperatingSpeed > maxOperatingSpeed) {
1059            log.warn("MaxOperatingSpeed [{}] < minReliableOperatingSpeed [{}] setting Max = Min",
1060                    minReliableOperatingSpeed, maxOperatingSpeed);
1061            this.maxOperatingSpeed = this.minReliableOperatingSpeed;
1062        }
1063    }
1064
1065    /**
1066     * Set min/max throttle limits, optionally enforcing a scale km/h cap. If
1067     * maxSpeedScaleKmh == 0.0f, the percent maxOperatingSpeed takes precedence
1068     * (no effect). If maxSpeedScaleKmh {@code >} 0.0f, we convert the km/h cap
1069     * to an equivalent throttle% using the roster profile and the layout scale
1070     * ratio, then take the minimum of that and the percent cap.
1071     *
1072     * @param minReliableOperatingSpeed lowest throttle % the loco reliably
1073     *                                  moves (0..1)
1074     * @param maxOperatingSpeed         percent cap (0..1)
1075     * @param maxSpeedScaleKmh          scale km/h cap; 0.0f means "unused"
1076     * @param layoutScaleRatio          layout scale ratio (full-scale / model),
1077     *                                  e.g. 87.0 for HO
1078     * @param isForward                 direction of travel
1079     */
1080    public void setMinMaxLimitsKmh(float minReliableOperatingSpeed,
1081            float maxOperatingSpeed,
1082            float maxSpeedScaleKmh,
1083            float layoutScaleRatio,
1084            boolean isForward) {
1085        // Default to the percent cap
1086        float maxPct = maxOperatingSpeed;
1087
1088        // If a km/h cap is specified and we have speeds for this direction, convert to throttle%
1089        boolean dirHasProfile = isForward ? hasForwardSpeeds() : hasReverseSpeeds();
1090        if (maxSpeedScaleKmh > 0.0f && dirHasProfile) {
1091            float safeScale = (layoutScaleRatio <= 0.0f) ? 1.0f : layoutScaleRatio;
1092            // Convert full-scale km/h -> model km/h -> model mm/s
1093            float modelKmh = maxSpeedScaleKmh / safeScale;
1094            float targetMms = modelKmh * 277.7778f; // 1 km/h = 277.7778 mm/s
1095
1096            float thrCapPct = getThrottleSetting(targetMms, isForward);
1097            if (thrCapPct > 0.0f) {
1098                maxPct = Math.min(maxOperatingSpeed, thrCapPct);
1099            }
1100        }
1101
1102        // Apply computed limits
1103        this.minReliableOperatingSpeed = minReliableOperatingSpeed;
1104        this.maxOperatingSpeed = maxPct;
1105
1106        // Guard: if min > max, clamp max to min (preserves previous method semantics)
1107        if (this.minReliableOperatingSpeed > this.maxOperatingSpeed) {
1108            log.warn("MaxOperatingSpeed [{}] < minReliableOperatingSpeed [{}]; setting Max = Min",
1109                    this.maxOperatingSpeed, this.minReliableOperatingSpeed);
1110            this.maxOperatingSpeed = this.minReliableOperatingSpeed;
1111        }
1112    }
1113
1114    /**
1115     * Set speed of a throttle.
1116     *
1117     * @param t     the throttle to set
1118     * @param blk   the block used for length details
1119     * @param speed the speed to set
1120     */
1121    public void changeLocoSpeed(DccThrottle t, Block blk, float speed) {
1122        if (blk == referenced && Float.compare(speed, desiredSpeedStep) == 0) {
1123            //log.debug("Already setting to desired speed step for this block");
1124            return;
1125        }
1126        float blockLength = blk.getLengthMm();
1127        if (blk == referenced) {
1128            distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000));
1129            blockLength = distanceRemaining;
1130            //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run
1131            log.debug("Block passed is the same as we are currently processing");
1132        } else {
1133            referenced = blk;
1134        }
1135        changeLocoSpeed(t, blockLength, speed);
1136    }
1137
1138    /**
1139     * Set speed of a throttle.
1140     *
1141     * @param t     the throttle to set
1142     * @param sec   the section used for length details
1143     * @param speed the speed to set
1144     * @param usePercentage the percentage of the block to be used for stopping
1145     */
1146    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY",
1147        justification = "OK to compare floats, as even tiny differences should trigger update")
1148    public void changeLocoSpeed(DccThrottle t, Section sec, float speed, float usePercentage) {
1149        if (sec == referenced && speed == desiredSpeedStep) {
1150            log.debug("Already setting to desired speed step for this Section");
1151            return;
1152        }
1153        float sectionLength = sec.getActualLength() * usePercentage;
1154        if (sec == referenced) {
1155            distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000));
1156            sectionLength = distanceRemaining;
1157            //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run
1158            log.debug("Block passed is the same as we are currently processing");
1159        } else {
1160            referenced = sec;
1161        }
1162        changeLocoSpeed(t, sectionLength, speed);
1163    }
1164
1165    /**
1166     * Set speed of a throttle.
1167     *
1168     * @param t     the throttle to set
1169     * @param blk   the block used for length details
1170     * @param speed the speed to set
1171     * @param usePercentage the percentage of the block to be used for stopping
1172     */
1173    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY",
1174        justification = "OK to compare floats, as even tiny differences should trigger update")
1175    public void changeLocoSpeed(DccThrottle t, Block blk, float speed, float usePercentage) {
1176        if (blk == referenced && speed == desiredSpeedStep) {
1177            //if(log.isDebugEnabled()) log.debug("Already setting to desired speed step for this block");
1178            return;
1179        }
1180        float blockLength = blk.getLengthMm() * usePercentage;
1181        if (blk == referenced) {
1182            distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000));
1183            blockLength = distanceRemaining;
1184            //Not entirely reliable at this stage as the loco could still be running and not completed the calculation of the distance, this could result in an over run
1185            log.debug("Block passed is the same as we are currently processing");
1186        } else {
1187            referenced = blk;
1188        }
1189        changeLocoSpeed(t, blockLength, speed);
1190
1191    }
1192
1193    /**
1194     * Set speed of a throttle to a speeed set by a float, using the section for
1195     * the length details
1196     * Set speed of a throttle.
1197     *
1198     * @param t     the throttle to set
1199     * @param sec   the section used for length details
1200     * @param speed the speed to set
1201     */
1202    //@TODO if a section contains multiple blocks then we could calibrate the change of speed based upon the block status change.
1203    public void changeLocoSpeed(DccThrottle t, Section sec, float speed) {
1204        if (sec == referenced && Float.compare(speed, desiredSpeedStep) == 0) {
1205            log.debug("Already setting to desired speed step for this section");
1206            return;
1207        }
1208        float sectionLength = sec.getActualLength();
1209        log.debug("call to change speed via section {}", sec.getDisplayName());
1210        if (sec == referenced) {
1211            distanceRemaining = distanceRemaining - getDistanceTravelled(_throttle.getIsForward(), _throttle.getSpeedSetting(), ((float) (System.nanoTime() - lastTimeTimerStarted) / 1000000000));
1212            sectionLength = distanceRemaining;
1213        } else {
1214            referenced = sec;
1215        }
1216
1217        changeLocoSpeed(t, sectionLength, speed);
1218    }
1219
1220    /**
1221     * Set speed of a throttle.
1222     *
1223     * @param t        the throttle to set
1224     * @param distance the distance in meters
1225     * @param requestedSpeed    the speed to set
1226     */
1227    public void changeLocoSpeed(DccThrottle t, float distance, float requestedSpeed) {
1228        float speed = 0.0f;
1229        log.debug("Call to change speed over specific distance: speed {} distance {}", requestedSpeed, distance);
1230        if (requestedSpeed  > maxOperatingSpeed) {
1231            speed = maxOperatingSpeed;
1232        } else {
1233            speed = requestedSpeed;
1234        }
1235        if (Float.compare(speed, desiredSpeedStep) == 0) {
1236            // This requires no checks for min/max.
1237            log.debug("Already setting to desired speed step");
1238            return;
1239        }
1240        log.debug("public change speed step by float {}", speed);
1241        log.debug("Desired Speed Step {} asked for {}", desiredSpeedStep, speed);
1242
1243        if (stopTimer != null) {
1244            log.debug("stop timer valid so will cancel");
1245            cancelSpeedChange();
1246        }
1247        _throttle = t;
1248        desiredSpeedStep = speed;
1249
1250        log.debug("Speed current {} required {} ",
1251                _throttle.getSpeedSetting(), speed);
1252        if (_throttle.getSpeedSetting() < speed) {
1253            log.debug("Going for acceleration");
1254        } else {
1255            log.debug("Going for deceleration");
1256        }
1257
1258        float adjSpeed = speed;
1259        boolean andStop = false;
1260        if (speed <= 0.0) {
1261            andStop = true;
1262        }
1263        if (speed < minReliableOperatingSpeed) {
1264            adjSpeed = minReliableOperatingSpeed;
1265        }
1266        log.debug("Speed[{}] adjSpeed[{}] MinSpeed[{}]",
1267                speed,adjSpeed, minReliableOperatingSpeed);
1268
1269        if (!andStop
1270                && (Float.compare(adjSpeed, t.getSpeedSetting()) == 0
1271                    || (Math.round(adjSpeed/t.getSpeedIncrement()) ==
1272                            Math.round(t.getSpeedSetting()/t.getSpeedIncrement())))) {
1273            log.debug("Throttle and request speed setting are the same {} {} so will quit", speed, t.getSpeedSetting());
1274            //Already at correct speed setting
1275            finishChange();
1276            return;
1277        }
1278        calculateStepDetails(adjSpeed, distance, andStop);
1279    }
1280
1281    private List<SpeedSetting> testSteps = new ArrayList<>();
1282    private boolean profileInTestMode = false;
1283
1284    void calculateStepDetails(float speedStep, float distance, boolean andStop) {
1285
1286        float stepIncrement = _throttle.getSpeedIncrement();
1287        log.debug("Desired Speed Step {} asked for {}", desiredSpeedStep, speedStep);
1288        desiredSpeedStep = speedStep;
1289        log.debug("calculated current step {} required {} current {} increment {}", _throttle.getSpeedSetting(), speedStep, _throttle.getSpeedSetting(), stepIncrement);
1290        boolean increaseSpeed = false;
1291        if (_throttle.getSpeedSetting() < speedStep) {
1292            increaseSpeed = true;
1293            log.debug("Going for acceleration");
1294        } else {
1295            log.debug("Going for deceleration");
1296        }
1297
1298        if (distance <= 0) {
1299            log.debug("Distance is less than 0 {}", distance);
1300            _throttle.setSpeedSetting(speedStep);
1301            finishChange();
1302            return;
1303        }
1304
1305        float calculatedDistance = distance;
1306
1307        if (stopTimer != null) {
1308            stopTimer.stop();
1309            distanceRemaining = distance;
1310        } else {
1311            calculatedDistance = calculateInitialOverRun(distance);
1312            distanceRemaining = calculatedDistance;
1313        }
1314        if (distanceRemaining < 0.0f) {
1315            if (andStop) {
1316                _throttle.setSpeedSetting(0.0f);
1317            } else {
1318                _throttle.setSpeedSetting(speedStep);
1319            }
1320            log.warn("There is insufficient distance [{}] after adjustments, setting speed immediately", distanceRemaining);
1321            return;
1322        }
1323
1324        float calculatingStep = _throttle.getSpeedSetting();
1325        if (increaseSpeed) {
1326            if (calculatingStep < minReliableOperatingSpeed) {
1327                calculatingStep = minReliableOperatingSpeed;
1328            }
1329        }
1330
1331        float endspd = 0;
1332        if (calculatingStep != 0.0 && desiredSpeedStep > 0) { // current speed
1333            if (_throttle.getIsForward()) {
1334                endspd = getForwardSpeed(desiredSpeedStep);
1335            } else {
1336                endspd = getReverseSpeed(desiredSpeedStep);
1337            }
1338        } else if (desiredSpeedStep != 0.0) {
1339            if (_throttle.getIsForward()) {
1340                endspd = getForwardSpeed(desiredSpeedStep);
1341            } else {
1342                endspd = getReverseSpeed(desiredSpeedStep);
1343            }
1344        }
1345
1346        boolean calculated = false;
1347        while (!calculated) {
1348            float spd = 0;
1349            if (calculatingStep != 0.0) { // current speed
1350                if (_throttle.getIsForward()) {
1351                    spd = getForwardSpeed(calculatingStep);
1352                } else {
1353                    spd = getReverseSpeed(calculatingStep);
1354                }
1355            }
1356
1357            log.debug("end spd {} spd {}", endspd, spd);
1358            double avgSpeed = Math.abs((endspd + spd) * 0.5);
1359            log.debug("avg Speed {}", avgSpeed);
1360
1361            double time = (calculatedDistance / avgSpeed); //in seconds
1362            time = time * 1000; //covert it to milli seconds
1363            float speeddiff = calculatingStep - desiredSpeedStep;
1364            if (increaseSpeed) {
1365                speeddiff =  desiredSpeedStep - calculatingStep;
1366            }
1367            float noSteps = speeddiff / stepIncrement;
1368            log.debug("Speed diff {} number of Steps {} step increment {}", speeddiff, noSteps, stepIncrement);
1369
1370            int timePerStep = (int) (time / noSteps);
1371            if (timePerStep < 0) {
1372                log.error("Time per speed went to zero or below, setting finale speed immediatly.");
1373                if (_throttle != null) {
1374                    addSpeedStepItem(calculated,new SpeedSetting(desiredSpeedStep, 10, andStop));
1375                    setNextStep();
1376                }
1377                break;
1378            }
1379            float calculatedStepInc = stepIncrement;
1380            boolean lastStep = false;
1381            if (Math.abs(speeddiff) > (stepIncrement * 2)) {
1382                //We do not get reliable time results if the duration per speed step is less than 500ms
1383                //therefore we calculate how many speed steps will fit in to 750ms.
1384                if (timePerStep <= 500 && timePerStep > 0) {
1385                    float newTime = 750.0f;
1386                    float tmp =(float) Math.floor(newTime / timePerStep);
1387                    // To avoid the lack of a stub ensure resultant speed is less than final speed by at least a step.
1388                    if (increaseSpeed) {
1389                        while (desiredSpeedStep - ( calculatingStep + (stepIncrement * tmp)) <= stepIncrement) {
1390                            tmp = tmp - 1;
1391                        }
1392
1393                        if (tmp > 0 && calculatedDistance - getDistanceTravelled(_throttle.getIsForward(),
1394                                    calculatingStep + (stepIncrement * tmp),
1395                                    ((float) (newTime / 1000.0))) > 0) {
1396                            calculatedStepInc = stepIncrement * tmp;
1397                            timePerStep = (int)newTime;
1398                        }
1399                    } else {
1400                        while (calculatingStep - (stepIncrement * tmp) - desiredSpeedStep <= stepIncrement) {
1401                            tmp = tmp - 1;
1402                        }
1403                        if ( tmp > 0 && (calculatedDistance
1404                                - getDistanceTravelled(_throttle.getIsForward(),
1405                                        calculatingStep - (stepIncrement * tmp),
1406                                        ((float) (newTime / 1000.0)))) > 0) {
1407                            calculatedStepInc = stepIncrement * tmp;
1408                            timePerStep = (int)newTime;
1409                        }
1410                    }
1411                    log.debug("time per step was {} no of increments in 750 ms is {} new step increment in {}", timePerStep, tmp, calculatedStepInc);
1412                }
1413            } else {
1414                // last bit calculate duration from distance remaining
1415                if (increaseSpeed && calculatingStep == 0) {
1416                    calculatingStep+=calculatedStepInc;
1417                }
1418                timePerStep = Math.round(calculatedDistance/getSpeed(calculatingStep,_throttle.getIsForward())*1000);
1419                if (!increaseSpeed) {
1420                    calculatedStepInc = calculatingStep - desiredSpeedStep;
1421                } else {
1422                    calculatedStepInc = desiredSpeedStep - calculatingStep ;
1423                }
1424                lastStep=true;
1425            }
1426            calculatedStepInc=Math.abs(calculatedStepInc);
1427            log.debug("per interval {}, increase {} lastStep {}", timePerStep, increaseSpeed,lastStep);
1428            //Calculate the new speed setting
1429            if (increaseSpeed) {
1430                //if (calculatingStep + calculatedStepInc == desiredSpeedStep) {
1431                if (lastStep) {
1432                    SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop);
1433                    addSpeedStepItem(calculated,ss);
1434                    calculated = true;
1435                    if (!andStop) { calculatingStep = desiredSpeedStep;timePerStep=2;}
1436                    else {
1437                        calculatingStep = 0.0f;timePerStep=2;
1438                    }
1439                    ss = new SpeedSetting(calculatingStep, timePerStep, andStop);
1440                    addSpeedStepItem(calculated,ss);
1441                    if (stopTimer == null) {
1442                        setNextStep();
1443                    }
1444                    break;
1445                }
1446                calculatingStep = calculatingStep + calculatedStepInc;
1447            } else {
1448                if (lastStep) {
1449                    SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop);
1450                    addSpeedStepItem(calculated,ss);
1451                    calculated = true;
1452                    if (!andStop) { calculatingStep = desiredSpeedStep;timePerStep=2;}
1453                    else {
1454                        calculatingStep = 0.0f;timePerStep=2;
1455                    }
1456                    ss = new SpeedSetting(calculatingStep, timePerStep, andStop);
1457                    addSpeedStepItem(calculated,ss);
1458                    if (stopTimer == null) { //If this is the first time round then kick off the speed change
1459                        setNextStep();
1460                    }
1461                    break;
1462                }
1463                calculatingStep = calculatingStep - calculatedStepInc;
1464            }
1465            SpeedSetting ss = new SpeedSetting(calculatingStep, timePerStep, andStop);
1466            addSpeedStepItem(calculated,ss);
1467            if (stopTimer == null) { //If this is the first time round then kick off the speed change
1468                setNextStep();
1469            }
1470            if (calculated) {
1471               if (andStop) {
1472                   ss = new SpeedSetting(0.0f, 10, andStop);
1473               } else {
1474                   ss = new SpeedSetting(desiredSpeedStep, 10, andStop);
1475               }
1476               addSpeedStepItem(calculated,ss);            }
1477            // The throttle can disappear during a stop situation
1478            if (_throttle != null) {
1479                calculatedDistance = calculatedDistance - getDistanceTravelled(_throttle.getIsForward(), calculatingStep, ((float) (timePerStep / 1000.0)));
1480            } else {
1481                log.warn("Throttle destroyed before zero length[{}] remaining.",calculatedDistance);
1482                calculatedDistance = 0;
1483            }
1484
1485            if (calculatedDistance <= 0 && !calculated) {
1486                log.warn("distance remaining is now 0, but we have not reached desired speed setting {} v {}", desiredSpeedStep, calculatingStep);
1487                calculated = true;
1488            }
1489        }
1490    }
1491
1492    private void addSpeedStepItem(Boolean calculated, SpeedSetting ss) {
1493        synchronized (this) {
1494            stepQueue.addLast(ss);
1495            if (profileInTestMode) {
1496                testSteps.add(ss);
1497            }
1498            if (ss.andStop && calculated) {
1499                ss = new SpeedSetting( 0.0f, 0, ss.andStop);
1500                stepQueue.addLast(ss);
1501                if (profileInTestMode) {
1502                    testSteps.add(ss);
1503                }
1504            }
1505        }
1506    }
1507
1508    //The bit with the distance is not used
1509    float calculateInitialOverRun(float distance) {
1510        log.debug("Stop timer not configured so will add overrun {}", distance);
1511        if (_throttle.getIsForward()) {
1512            float extraAsDouble = (getOverRunTimeForward() + extraDelay) / 1000;
1513            if (log.isDebugEnabled()) {
1514                log.debug("Over run time to remove (Forward) {} {}", getOverRunTimeForward(), extraAsDouble);
1515            }
1516            float olddistance = getDistanceTravelled(true, _throttle.getSpeedSetting(), extraAsDouble);
1517            distance = distance - olddistance;
1518            //time = time-getOverRunTimeForward();
1519            //time = time-(extraAsDouble*1000);
1520        } else {
1521            float extraAsDouble = (getOverRunTimeReverse() + extraDelay) / 1000;
1522            if (log.isDebugEnabled()) {
1523                log.debug("Over run time to remove (Reverse) {} {}", getOverRunTimeReverse(), extraAsDouble);
1524            }
1525            float olddistance = getDistanceTravelled(false, _throttle.getSpeedSetting(), extraAsDouble);
1526            distance = distance - olddistance;
1527            //time = time-getOverRunTimeReverse();
1528            //time = time-(extraAsDouble*1000);
1529        }
1530        log.debug("Distance remaining {}", distance);
1531        //log.debug("Time after overrun removed " + time);
1532        return distance;
1533
1534    }
1535
1536    /**
1537     * This method is called to cancel the existing change in speed.
1538     */
1539    public void cancelSpeedChange() {
1540        if (stopTimer != null && stopTimer.isRunning()) {
1541            stopTimer.stop();
1542        }
1543        finishChange();
1544    }
1545
1546    synchronized void setNextStep() {
1547        //if (profileInTestMode) {
1548        //    return;
1549        //}
1550        if (stepQueue.isEmpty()) {
1551            log.debug("No more results");
1552            finishChange();
1553            return;
1554        }
1555        SpeedSetting ss = stepQueue.getFirst();
1556        if (ss.getDuration() == 0) {
1557            if (ss.getAndStop()) {
1558                _throttle.setSpeedSetting(0.0f);
1559            } else {
1560                _throttle.setSpeedSetting(desiredSpeedStep);
1561            }
1562            finishChange();
1563            return;
1564        }
1565        if (stopTimer != null) {
1566            //Reduce the distanceRemaining and calculate the distance travelling
1567            float distanceTravelledThisStep = getDistanceTravelled(_throttle.getIsForward(),
1568                    getEffectiveCurrentSpeedSetting(), ((float) (stopTimer.getDelay() / 1000.0)));
1569            distanceTravelled = distanceTravelled + distanceTravelledThisStep;
1570            distanceRemaining = distanceRemaining - distanceTravelledThisStep;
1571        }
1572        stepQueue.removeFirst();
1573        lastIssuedSpeedSetting = ss.getSpeedStep();
1574        _throttle.setSpeedSetting(lastIssuedSpeedSetting);
1575        stopTimer = new javax.swing.Timer(ss.getDuration(), (java.awt.event.ActionEvent e) -> {
1576            setNextStep();
1577        });
1578        stopTimer.setRepeats(false);
1579        lastTimeTimerStarted = System.nanoTime();
1580        stopTimer.start();
1581
1582    }
1583
1584    private LinkedList<SpeedSetting> stepQueue = new LinkedList<>();
1585
1586    public static class SpeedSetting {
1587
1588        private float step = 0.0f;
1589        private int duration = 0;
1590        private boolean andStop;
1591
1592        public SpeedSetting(float step, int duration, boolean andStop) {
1593            log.debug("Adding step {} duration {} andStop{}", step, duration, andStop);
1594            this.step = step;
1595            this.duration = duration;
1596            this.andStop = andStop;
1597        }
1598
1599        public float getSpeedStep() {
1600            return step;
1601        }
1602
1603        public int getDuration() {
1604            return duration;
1605        }
1606
1607        public boolean getAndStop() {
1608            return andStop;
1609        }
1610    }
1611
1612    /*
1613     * The follow deals with the storage and loading of the speed profile for a roster entry.
1614     */
1615    public void store(Element e) {
1616        Element d = new Element("speedprofile");
1617        d.addContent(new Element("overRunTimeForward").addContent(Float.toString(getOverRunTimeForward())));
1618        d.addContent(new Element("overRunTimeReverse").addContent(Float.toString(getOverRunTimeReverse())));
1619        Element s = new Element("speeds");
1620        speeds.keySet().stream().forEachOrdered( i -> {
1621            Element ss = new Element("speed");
1622            ss.addContent(new Element("step").addContent(Integer.toString(i)));
1623            ss.addContent(new Element("forward").addContent(Float.toString(speeds.get(i).getForwardSpeed())));
1624            ss.addContent(new Element("reverse").addContent(Float.toString(speeds.get(i).getReverseSpeed())));
1625            s.addContent(ss);
1626        });
1627        d.addContent(s);
1628        e.addContent(d);
1629    }
1630
1631    public void load(Element e) {
1632        try {
1633            setOverRunTimeForward(Float.parseFloat(e.getChild("overRunTimeForward").getText()));
1634        } catch (NumberFormatException ex) {
1635            log.error("Over run Error For {}", _re.getId());
1636        }
1637        try {
1638            setOverRunTimeReverse(Float.parseFloat(e.getChild("overRunTimeReverse").getText()));
1639        } catch (NumberFormatException ex) {
1640            log.error("Over Run Error Rev {}", _re.getId());
1641        }
1642        e.getChild("speeds").getChildren("speed").forEach( spd -> {
1643            try {
1644                String step = spd.getChild("step").getText();
1645                String forward = spd.getChild("forward").getText();
1646                String reverse = spd.getChild("reverse").getText();
1647                float forwardSpeed = Float.parseFloat(forward);
1648                if (forwardSpeed > 0.0f) {
1649                    _hasForwardSpeeds = true;
1650                }
1651                float reverseSpeed = Float.parseFloat(reverse);
1652                if (reverseSpeed > 0.0f) {
1653                    _hasReverseSpeeds = true;
1654                }
1655                setSpeed(Integer.parseInt(step), forwardSpeed, reverseSpeed);
1656            } catch (NumberFormatException ex) {
1657                log.error("Not loaded {}", ex.getMessage());
1658            }
1659        });
1660    }
1661
1662    public static class SpeedStep {
1663
1664        private float forward = 0.0f;
1665        private float reverse = 0.0f;
1666
1667        /**
1668         * Create a new SpeedStep, Reverse and Forward speeds are 0.
1669         */
1670        public SpeedStep() {
1671        }
1672
1673        /**
1674         * Set the Forward speed for the step.
1675         * @param speed the forward speed for the Step.
1676         */
1677        public void setForwardSpeed(float speed) {
1678            forward = speed;
1679        }
1680
1681        /**
1682         * Set the Reverse speed for the step.
1683         * @param speed the reverse speed for the Step.
1684         */
1685        public void setReverseSpeed(float speed) {
1686            reverse = speed;
1687        }
1688
1689        /**
1690         * Get the Forward Speed for the Step.
1691         * @return the forward speed.
1692         */
1693        public float getForwardSpeed() {
1694            return forward;
1695        }
1696
1697        /**
1698         * Get the Reverse Speed for the Step.
1699         * @return the reverse speed.
1700         */
1701        public float getReverseSpeed() {
1702            return reverse;
1703        }
1704
1705        @Override
1706        public boolean equals(Object obj) {
1707            if (this == obj) {
1708                return true;
1709            }
1710            if (obj == null || getClass() != obj.getClass()) {
1711                return false;
1712            }
1713            SpeedStep ss = (SpeedStep) obj;
1714            return Float.compare(ss.getForwardSpeed(), forward) == 0
1715                && Float.compare(ss.getReverseSpeed(), reverse) == 0;
1716        }
1717
1718            @Override
1719            public int hashCode() {
1720                int result = 17;
1721                result = 31 * result + Float.floatToIntBits(forward);
1722                result = 31 * result + Float.floatToIntBits(reverse);
1723                return result;
1724        }
1725
1726    }
1727
1728    /**
1729     * Get the number of SpeedSteps.
1730     * If there are too few SpeedSteps, it may be difficult to get reasonable
1731     * distances and speeds over a large range of throttle settings.
1732     * @return the number of Speed Steps in the profile.
1733     */
1734    public int getProfileSize() {
1735        return speeds.size();
1736    }
1737
1738    public TreeMap<Integer, SpeedStep> getProfileSpeeds() {
1739        return speeds;
1740    }
1741
1742    /**
1743     * Get the throttle setting to achieve a track speed
1744     *
1745     * @param speed     desired track speed in mm/sec
1746     * @param isForward direction
1747     * @return throttle setting
1748     */
1749    public float getThrottleSetting(float speed, boolean isForward) {
1750        if ((isForward && !_hasForwardSpeeds) || (!isForward && !_hasReverseSpeeds)) {
1751            return 0.0f;
1752        }
1753        int slowerKey = 0;
1754        float slowerValue = 0;
1755        float fasterKey;
1756        float fasterValue;
1757        Entry<Integer, SpeedStep> entry = speeds.firstEntry();
1758        if (entry == null) {
1759            log.warn("There is no speedprofile entries for [{}]", this.getRosterEntry().getId());
1760            return (0.0f);
1761        }
1762        // search through table until end or the entry is greater than
1763        // what we are looking for. This leaves the previous lower value in key. and slower
1764        // Note there may be zero values interspersed in the tree
1765        if (isForward) {
1766            fasterKey = entry.getKey();
1767            fasterValue = entry.getValue().getForwardSpeed();
1768            while (entry != null && entry.getValue().getForwardSpeed() < speed) {
1769                slowerKey = entry.getKey();
1770                float value = entry.getValue().getForwardSpeed();
1771                if (value > 0.0f) {
1772                    slowerValue = value;
1773                }
1774                entry = speeds.higherEntry(slowerKey);
1775                if (entry != null) {
1776                    fasterKey = entry.getKey();
1777                    value = entry.getValue().getForwardSpeed();
1778                    if (value > 0.0f) {
1779                        fasterValue = value;
1780                    }
1781                }
1782            }
1783        } else {
1784            fasterKey = entry.getKey();
1785            fasterValue = entry.getValue().getReverseSpeed();
1786            while (entry != null && entry.getValue().getReverseSpeed() < speed) {
1787                slowerKey = entry.getKey();
1788                float value = entry.getValue().getReverseSpeed();
1789                if (value > 0.0f) {
1790                    slowerValue = value;
1791                }
1792                entry = speeds.higherEntry(slowerKey);
1793                if (entry != null) {
1794                    fasterKey = entry.getKey();
1795                    value = entry.getValue().getReverseSpeed();
1796                    if (value > 0.0f) {
1797                        fasterValue = value;
1798                    }
1799                }
1800            }
1801        }
1802        log.trace("slowerKey={}, slowerValue={} fasterKey={} fasterValue={} for speed={}",
1803                slowerKey, slowerValue, fasterKey, fasterValue, speed);
1804        if (entry == null) {
1805            // faster does not exists use slower...
1806            if (slowerValue <= 0.0f) { // neither does slower
1807                return (0.0f);
1808            }
1809
1810            // extrapolate
1811            float key = slowerKey * speed / slowerValue;
1812            if (key < 1000.0f) {
1813                return key / 1000.0f;
1814            } else {
1815                return 1.0f;
1816            }
1817        }
1818        if (Float.compare(slowerValue, speed) == 0 || fasterValue <= slowerValue) {
1819            return slowerKey / 1000.0f;
1820        }
1821        if (slowerValue <= 0.0f) {  // no entry had a slower speed, therefore key is invalid
1822            slowerKey = 0;
1823            if (fasterValue <= 0.0f) {  // neither is there a faster speed
1824                return (0.0f);
1825            }
1826        }
1827        // we need to interpolate
1828        float ratio = (speed - slowerValue) / (fasterValue - slowerValue);
1829        return (slowerKey + ((fasterKey - slowerKey) * ratio)) / 1000.0f;
1830    }
1831
1832    /**
1833     * Get track speed in millimeters per second from throttle setting
1834     *
1835     * @param speedStep  throttle setting
1836     * @param isForward  direction
1837     * @return track speed
1838     */
1839    public float getSpeed(float speedStep, boolean isForward) {
1840        if (speedStep < 0.00001f) {
1841            return 0.0f;
1842        }
1843        float speed;
1844        if (isForward) {
1845            speed = getForwardSpeed(speedStep);
1846        } else {
1847            speed = getReverseSpeed(speedStep);
1848        }
1849        return speed;
1850    }
1851
1852    /**
1853     * Physics-based acceleration to a target throttle percent (0..1). Builds
1854     * and runs a throttle/time schedule using this profile's
1855     * stepQueue/stopTimer.
1856     *
1857     * @param t                      The DccThrottle to drive (must not be null)
1858     * @param targetThrottlePct      Desired throttle percent [0..1]
1859     * @param driverPowerPercent     Driver power/regulator percent [0..1]
1860     *                               (limits applied power/TE during
1861     *                               acceleration)
1862     * @param additionalWeightTonnes Extra consist mass in metric tonnes
1863     *                               ({@code >=} 0)
1864     * @param rollingResistanceCoeff Rolling resistance coefficient c_rr
1865     *                               ({@code >=} 0), e.g., ~0.002
1866     * @param layoutScaleRatio       Layout scale ratio (full-scale / model),
1867     *                               e.g., 87.0 for HO
1868     * @param speedFactor            If {@code >} 0, throttle commands are
1869     *                               divided by this factor before enqueuing
1870     */
1871    public void runPhysicsAccelerationToTargetThrottle(
1872            jmri.DccThrottle t,
1873            float targetThrottlePct,
1874            float driverPowerPercent,
1875            float additionalWeightTonnes,
1876            float rollingResistanceCoeff,
1877            float layoutScaleRatio,
1878            float speedFactor) {
1879        if (t == null) {
1880            log.warn("runPhysicsAccelerationToTargetThrottle called with null throttle; ignoring.");
1881            return;
1882        }
1883        float speedFactorSafe = (speedFactor > 0.0f) ? speedFactor : 1.0f;
1884        float driverPct = clampPct(driverPowerPercent);
1885        float crr = (rollingResistanceCoeff < 0.0f) ? 0.0f : rollingResistanceCoeff;
1886        float scaleRatio = (layoutScaleRatio <= 0.0f) ? 1.0f : layoutScaleRatio;
1887
1888        final boolean forward = t.getIsForward();
1889        final float minPct = this.minReliableOperatingSpeed;
1890        final float maxPct = this.maxOperatingSpeed;
1891
1892        // Kill any running timer and clear queue (do NOT call finishChange() which resets limits)
1893        if (stopTimer != null) {
1894            stopTimer.stop();
1895            stopTimer = null;
1896        }
1897        synchronized (this) {
1898            stepQueue = new LinkedList<>();
1899        }
1900        _throttle = t;
1901        lastIssuedSpeedSetting = quantizeToSpeedStep(_throttle, clampPct(_throttle.getSpeedSetting()));
1902
1903        float thrNow = lastIssuedSpeedSetting;
1904        float v0_mms = getSpeed(thrNow, forward);
1905        float vTarget_mms = getSpeed(clampPct(targetThrottlePct), forward);
1906
1907        float v0_fs = (v0_mms / 1000.0f) * scaleRatio;
1908        float vTarget_fs = (vTarget_mms / 1000.0f) * scaleRatio;
1909
1910        float vMin_mms = getSpeed(Math.max(0.0f, minPct), forward);
1911        float vMin_fs = (vMin_mms / 1000.0f) * scaleRatio;
1912        if (vTarget_fs < vMin_fs)
1913            vTarget_fs = vMin_fs;
1914        if (v0_fs < vMin_fs)
1915            v0_fs = vMin_fs;
1916
1917        float vCap_fs_roster = Float.POSITIVE_INFINITY;
1918        try {
1919            float kmhRoster = (_re != null) ? _re.getPhysicsMaxSpeedKmh() : 0.0f;
1920            if (kmhRoster > 0.0f)
1921                vCap_fs_roster = kmhRoster / 3.6f;
1922        } catch (Throwable ex) {
1923            log.debug("runPhysicsAccelerationToTargetThrottle: could not read roster max speed cap", ex);
1924        }
1925        vTarget_fs = Math.min(vTarget_fs, vCap_fs_roster);
1926
1927        float massKg = 1000.0f;
1928        float powerW = 0.0f;
1929        float teN = 0.0f;
1930        boolean mechTransmission = false;
1931        boolean isSteam = false;
1932        try {
1933            float rosterKg = (_re != null) ? _re.getPhysicsWeightKg() : 0.0f;
1934            float extraKg = Math.max(0.0f, additionalWeightTonnes) * 1000.0f;
1935            massKg = Math.max(1.0f, rosterKg + extraKg);
1936            powerW = (_re != null) ? (_re.getPhysicsPowerKw() * 1000.0f) : 0.0f;
1937            teN = (_re != null) ? (_re.getPhysicsTractiveEffortKn() * 1000.0f) : 0.0f;
1938            mechTransmission = (_re != null) && _re.isPhysicsMechanicalTransmission();
1939            jmri.jmrit.roster.RosterEntry.TractionType tt =
1940                    (_re != null) ? _re.getPhysicsTractionType()
1941                            : jmri.jmrit.roster.RosterEntry.TractionType.DIESEL_ELECTRIC;
1942            isSteam = (tt == jmri.jmrit.roster.RosterEntry.TractionType.STEAM);
1943        } catch (Throwable ex) {
1944            log.warn("RosterEntry missing physics fields; falling back to immediate set.", ex);
1945        }
1946
1947        final float powerExpSteam = 0.85f;
1948        float alphaPower =
1949                isSteam ? (driverPct <= 0.0f ? 0.0f : (float) Math.pow(driverPct, powerExpSteam)) : driverPct;
1950        float alphaTE = driverPct <= 0.0f ? 0.0f : driverPct;
1951        float P_avail = powerW * alphaPower;
1952        float TE_avail = teN * alphaTE;
1953
1954        final int internalSliceMs = 50;
1955        final float dt = internalSliceMs / 1000.0f;
1956        final int minCmdMs = getEffectiveMinCommandIntervalMs();
1957
1958        java.util.LinkedList<SpeedSetting> plan = new java.util.LinkedList<>();
1959
1960        float v_fs = v0_fs;
1961
1962        final float[] gearFsMps = new float[]{
1963                15f * 0.44704f,
1964                27f * 0.44704f,
1965                41f * 0.44704f
1966        };
1967        boolean[] gearPauseDone = new boolean[gearFsMps.length];
1968        for (int gi = 0; gi < gearPauseDone.length; gi++) {
1969            gearPauseDone[gi] = (v_fs >= gearFsMps[gi]);
1970        }
1971
1972        // Bucket accumulator for rate limiting.
1973        int bucketMs = 0;
1974        float bucketSpeedTime = 0.0f; // sum(mms * seconds)
1975
1976        int safety = 0;
1977        while (v_fs < vTarget_fs && safety < 10000) {
1978            float v_guard = Math.max(0.01f, v_fs);
1979            float F_power = (P_avail > 0.0f) ? (P_avail / v_guard) : 0.0f;
1980            float F_drive = (TE_avail > 0.0f) ? Math.min(TE_avail, F_power) : F_power;
1981
1982            final float g = 9.80665f;
1983            float F_rr = crr * massKg * g;
1984            float a_fs = (F_drive - F_rr) / massKg;
1985            if (a_fs < 0.0f)
1986                a_fs = 0.0f;
1987
1988            float stepDt = dt;
1989            float v_next_fs = v_fs + a_fs * stepDt;
1990            boolean finalStep = false;
1991            if (a_fs > 0.0f && v_next_fs > vTarget_fs) {
1992                stepDt = Math.max(0.001f, (vTarget_fs - v_fs) / a_fs);
1993                finalStep = true;
1994                v_next_fs = v_fs + a_fs * stepDt;
1995            }
1996
1997            // Gear-change pause (coast) if mechanical transmission and crossing a threshold
1998            boolean pausedThisSlice = false;
1999            if (mechTransmission) {
2000                for (int gi = 0; gi < gearFsMps.length; gi++) {
2001                    if (!gearPauseDone[gi]) {
2002                        float sTrig = gearFsMps[gi];
2003                        if ((vTarget_fs >= sTrig) && (v_fs < sTrig) && (v_next_fs >= sTrig)) {
2004                            final float pauseSec = 3.5f;
2005                            float left = pauseSec;
2006                            float aCoast_fs = -(crr * g);
2007                            while (left > 0.0f) {
2008                                float chunk = Math.min(dt, left);
2009                                float v_next_coast = v_fs + aCoast_fs * chunk;
2010                                if (v_next_coast < vMin_fs)
2011                                    v_next_coast = vMin_fs;
2012                                float v_mid_coast_fs = 0.5f * (v_fs + v_next_coast);
2013                                float v_mid_model_ms = v_mid_coast_fs / scaleRatio;
2014                                float v_mid_mms = v_mid_model_ms * 1000.0f;
2015
2016                                int ms = Math.max(1, Math.round(chunk * 1000.0f));
2017                                bucketMs += ms;
2018                                bucketSpeedTime += v_mid_mms * (ms / 1000.0f);
2019
2020                                if (bucketMs >= minCmdMs) {
2021                                    float bucketSec = bucketMs / 1000.0f;
2022                                    float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f;
2023                                    float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct);
2024                                    thr = clampPct(thr / speedFactorSafe);
2025                                    thr = quantizeToSpeedStep(_throttle, thr);
2026                                    plan.add(new SpeedSetting(thr, bucketMs, false));
2027                                    bucketMs = 0;
2028                                    bucketSpeedTime = 0.0f;
2029                                }
2030
2031                                v_fs = v_next_coast;
2032                                left -= chunk;
2033                                safety++;
2034                                if (safety >= 10000)
2035                                    break;
2036                            }
2037                            gearPauseDone[gi] = true;
2038                            pausedThisSlice = true;
2039                            break;
2040                    }
2041                }
2042            }
2043        }
2044        if (pausedThisSlice) {
2045            continue;
2046        }
2047
2048        float v_mid_fs = 0.5f * (v_fs + v_next_fs);
2049        float v_mid_model_ms = v_mid_fs / scaleRatio;
2050        float v_mid_mms = v_mid_model_ms * 1000.0f;
2051
2052        int ms = Math.max(1, Math.round(stepDt * 1000.0f));
2053        bucketMs += ms;
2054        bucketSpeedTime += v_mid_mms * (ms / 1000.0f);
2055
2056        // flush bucket
2057        if (bucketMs >= minCmdMs || finalStep) {
2058            float bucketSec = bucketMs / 1000.0f;
2059            float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f;
2060            float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct);
2061            thr = clampPct(thr / speedFactorSafe);
2062            thr = quantizeToSpeedStep(_throttle, thr);
2063            plan.add(new SpeedSetting(thr, bucketMs, false));
2064            bucketMs = 0;
2065            bucketSpeedTime = 0.0f;
2066        }
2067
2068        v_fs = v_next_fs;
2069        safety++;
2070        if (finalStep)
2071            break;
2072    }
2073
2074    // Flush any remaining bucket content
2075    if (bucketMs > 0) {
2076        float bucketSec = bucketMs / 1000.0f;
2077        float avgMms = (bucketSec > 0.0f) ? (bucketSpeedTime / bucketSec) : 0.0f;
2078        float thr = throttleForSpeedMms(avgMms, forward, minPct, maxPct);
2079        thr = clampPct(thr / speedFactorSafe);
2080        thr = quantizeToSpeedStep(_throttle, thr);
2081        plan.add(new SpeedSetting(thr, bucketMs, false));
2082    }
2083
2084    if (plan.isEmpty()) {
2085        float thrFinal = throttleForSpeedMms(vTarget_mms, forward, minPct, maxPct);
2086        thrFinal = clampPct(thrFinal / speedFactorSafe);
2087        thrFinal = quantizeToSpeedStep(_throttle, thrFinal);
2088        lastIssuedSpeedSetting = thrFinal;
2089        _throttle.setSpeedSetting(thrFinal);
2090        return;
2091    }
2092
2093    synchronized (this) {
2094        for (SpeedSetting ss : plan) {
2095            stepQueue.addLast(ss);
2096            if (profileInTestMode)
2097                testSteps.add(ss);
2098        }
2099    }
2100    if (stopTimer == null) {
2101        setNextStep();
2102    }
2103}
2104    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterSpeedProfile.class);
2105
2106}