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