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