001package jmri.jmrit.dispatcher;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.beans.PropertyChangeEvent;
005import java.beans.PropertyChangeListener;
006import java.util.LinkedList;
007
008import javax.annotation.CheckForNull;
009
010import jmri.*;
011import jmri.implementation.SignalSpeedMap;
012import jmri.jmrit.dispatcher.ActiveTrain.TrainDetection;
013import jmri.jmrit.roster.RosterEntry;
014import jmri.util.swing.JmriJOptionPane;
015
016/**
017 * This class holds information and options for an ActiveTrain when it is
018 * running in AUTOMATIC mode. It is an extension to Active Train for automatic
019 * running.
020 * <p>
021 * This class implements logic that follows a train around a layout. Train
022 * follows signals, provided the next Section is allocated to it, and its
023 * ActiveTrain's status is RUNNING.
024 * <p>
025 * This class is linked via its parent ActiveTrain object.
026 * <p>
027 * This file is part of JMRI.
028 * <p>
029 * JMRI is open source software; you can redistribute it and/or modify it under
030 * the terms of version 2 of the GNU General Public License as published by the
031 * Free Software Foundation. See the "COPYING" file for a copy of this license.
032 * <p>
033 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
034 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
035 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
036 * <p>
037 * The AutoEngineer sub class is based in part on code by Pete Cressman
038 * contained in Warrants.java
039 *
040 * @author Dave Duchamp Copyright (C) 2010-2011
041 */
042public class AutoActiveTrain implements ThrottleListener {
043
044    /**
045     * Create an AutoActiveTrain.
046     *
047     * @param at the train to automate
048     */
049    public AutoActiveTrain(ActiveTrain at) {
050        _activeTrain = at;
051        at.setAutoActiveTrain(this);
052        _autoTrainAction = new AutoTrainAction(this);
053        _lbManager = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class);
054        // listen for additions in our allocated section table
055        at.addPropertyChangeListener("sectionallocated",this::handleAnotherSectionAllocatedChange);
056    }
057
058    /* Speed aspects as defined by Douglas A. Kerr - "Rail Signal Aspects and Indications"
059     * http://dougkerr.net/Pumpkin/articles/Rail_signal_aspects.pdf (from Pete Cressman)
060     */
061    //    public static final int SPEED_MASK = 0x07;     // least significant 3 bits
062    public static final int STOP_SPEED = 0x01;     // No Speed
063    public static final int RESTRICTED_SPEED = 0x02;    // Train able to stop within 1/2 visual range (10mph)
064    public static final int SLOW_SPEED = 0x03;     // Typically 15 mph  (25% of NORMAL)
065    public static final int MEDIUM_SPEED = 0x04;     // Typically 30 mph (40% of NORMAL)
066    public static final int LIMITED_SPEED = 0x05;     // Typically 40-45 mph  (65% of NORMAL)
067    public static final int NORMAL_SPEED = 0x06;     // Varies with road and location
068    public static final int MAXIMUM_SPEED = 0x07;     // "full" throttle
069
070    private final Float[] _speedRatio = {-1.0F, 0.0F, 0.25F, 0.35F, 0.50F, 0.65F, 0.8F, 1.15F};
071
072    /* The ramp rates below are in addition to what the decoder itself does
073     */
074    public static final int RAMP_NONE = 0x00;  // No ramping - set speed immediately
075    public static final int RAMP_FAST = 0x01;     // Fast ramping
076    public static final int RAMP_MEDIUM = 0x02;  // Medium ramping
077    public static final int RAMP_MED_SLOW = 0x03;  // Medium/slow ramping
078    public static final int RAMP_SLOW = 0x04;  // Slow ramping
079    public static final int RAMP_SPEEDPROFILE = 0x05; // use speed profile and section distance
080    public static final int RAMP_PHYSICS = 0x06; // physics-based acceleration
081
082    /* Stop tasks codes
083     */
084    public static final int NO_TASK = 0x00;     // No task at stop
085    public static final int END_REVERSAL = 0x01;     // Handle reversing direction at end for back and forth running
086    public static final int BEGINNING_RESET = 0x02;     // Handle reseting beginning for back and forth running
087    public static final int END_TRAIN = 0x04;     // Ending Transit.
088
089    // operational instance variables
090    private static final NamedBean.DisplayOptions USERSYS = NamedBean.DisplayOptions.USERNAME_SYSTEMNAME;
091    private ActiveTrain _activeTrain = null;
092    private AutoTrainAction _autoTrainAction = null;
093    private DccThrottle _throttle = null;
094    private AutoEngineer _autoEngineer = null;
095    private int _address = -1;
096    private DccLocoAddress _dccAddress;
097    private int _savedStatus = ActiveTrain.RUNNING;
098    private int _currentRampRate = RAMP_NONE; // current Ramp Rate
099    private boolean _pausingActive = false;   // true if train pausing thread is active
100    private DispatcherFrame _dispatcher;
101
102    // persistent instance variables (saved with train info)
103    private int _rampRate = RAMP_NONE; // default Ramp Rate
104    private float _speedFactor = 1.0f; // default speed factor
105    private float _maxSpeed = 1.0f;    // default maximum train speed
106    // Maximum speed in scale km/h (0.0f = disabled; use throttle % cap)
107    private float _maxSpeedScaleKmh = 0.0f;
108    private float _minReliableOperatingSpeed = 0.0f;
109    private boolean _runInReverse = false;    // true if the locomotive should run through Transit in reverse
110    private boolean _soundDecoder = false;    // true if locomotive has a sound decoder
111    private long _MaxTrainLength = 600; // default train length mm.
112    private float _stopBySpeedProfileAdjust = 1.0f;
113    private boolean _stopBySpeedProfile = false;
114    // Distance-based stopping (HEAD/TAIL reference) — runtime memory
115    private float _stopByDistanceMm = 0.0f;          // 0.0f => feature disabled
116    private boolean _stopByDistanceRefTail = false;  // false => HEAD; true => TAIL
117
118    /** Returns the configured distance to stop into the block (mm); 0.0f means disabled.
119     * @return _stopByDistanceRefTail */
120    public boolean isStopByDistanceRefTail() {
121        return _stopByDistanceRefTail;
122    }
123    public float getStopByDistanceMm() {
124        return _stopByDistanceMm;
125    }
126
127    /** Sets whether the stop reference is TAIL (true) or HEAD (false). */
128    public void setStopByDistanceRefTail(boolean tail) { _stopByDistanceRefTail = tail; }
129
130    /** Sets the configured distance to stop into the block (mm). */
131    public void setStopByDistanceMm(float mm) { _stopByDistanceMm = (mm > 0.0f) ? mm : 0.0f; }
132
133    /** Returns true if the stop reference is TAIL (add train length); false for HEAD. */
134    private boolean _useSpeedProfileRequested = true;
135    private int _functionLight = 0;
136    private int _functionBell = 1;
137    private int _functionHorn = 2;
138
139    // accessor functions
140    public ActiveTrain getActiveTrain() {
141        return _activeTrain;
142    }
143
144    public DccLocoAddress getDccAddress() {
145        return _dccAddress;
146    }
147
148    public AutoEngineer getAutoEngineer() {
149        return _autoEngineer;
150    }
151
152    public AutoTrainAction getAutoTrainAction() {
153        return _autoTrainAction;
154    }
155
156    public RosterEntry getRosterEntry() {
157        return re;
158    }
159
160    public boolean getForward() {
161        return _autoEngineer.getIsForward();
162    }
163
164    public void setForward(boolean set) {
165        _autoEngineer.setIsForward(set);
166    }
167
168    /**
169     * Manually set the train throttle Function value.
170     * Value passed through to the Throttle.
171     * @param functionNum the function number.
172     * @param isSet true is on, false is off.
173     */
174    public void setFunction(int functionNum, boolean isSet) {
175        _autoEngineer.setFunction(functionNum, isSet);
176    }
177
178    public synchronized float getTargetSpeed() {
179        return _autoEngineer.getTargetSpeed();
180    }
181
182    public synchronized void setTargetSpeedByPass(float speed) {
183        _autoEngineer.setTargetSpeed(-1.0f, speed);
184    }
185
186    public synchronized void setTargetSpeedByPass(float distance, float speed) {
187        if (distance < 0.0f) {
188            _autoEngineer.setTargetSpeed(speed);
189        } else {
190            _autoEngineer.setTargetSpeed(distance, speed);
191        }
192    }
193
194    public synchronized void setTargetSpeed(float speed) {
195        log.debug("{}: setTargetSpeed(speed={}): stopped={}, targetWas={}, delayedMode=?",
196                _activeTrain.getTrainName(), speed, _autoEngineer.isStopped(), getTargetSpeed());
197        if (_autoEngineer.isStopped() && getTargetSpeed() == 0.0f && speed > 0.0f) {
198            boolean hold = _autoTrainAction.isDelayedStart(-1.0f, speed);
199            log.debug("{}: delayedStart check (no-distance): hold={}", _activeTrain.getTrainName(), hold);
200            if (_autoTrainAction.isDelayedStart(-1.0f, speed))
201                return;
202        }
203        _autoEngineer.setTargetSpeed(speed);
204    }
205
206    public synchronized void setTargetSpeed(float distance, float speed) {
207        log.debug("{}: setTargetSpeed(distance={}, speed={}): stopped={}, targetWas={}",
208                _activeTrain.getTrainName(), distance, speed, _autoEngineer.isStopped(), getTargetSpeed());
209        if (_autoEngineer.isStopped() && getTargetSpeed() == 0.0f && speed > 0.0f) {
210            if (_autoTrainAction.isDelayedStart(distance, speed))
211                return;
212        }
213        _autoEngineer.setTargetSpeed(distance, speed);
214    }
215
216    public int getSavedStatus() {
217        return _savedStatus;
218    }
219
220    public void setSavedStatus(int status) {
221        _savedStatus = status;
222    }
223
224    public synchronized void setCurrentRampRate(int rate) {
225        _currentRampRate = rate;
226    }
227
228    public int getRampRate() {
229        return _rampRate;
230    }
231
232    public void setRampRate(int rate) {
233        _rampRate = rate;
234        _currentRampRate = rate;
235    }
236
237    public float getSpeedFactor() {
238        return _speedFactor;
239    }
240
241    public void setSpeedFactor(float factor) {
242        _speedFactor = factor;
243    }
244
245    public float getMaxSpeed() {
246        return _maxSpeed;
247    }
248
249    public void setMaxSpeed(float speed) {
250        _maxSpeed = speed;
251        if (_autoEngineer != null ) {
252            _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor);
253        }
254    }
255
256    public float getMaxSpeedScaleKmh() { return _maxSpeedScaleKmh; }
257    public void setMaxSpeedScaleKmh(float kmh) { _maxSpeedScaleKmh = kmh; }
258
259    /**
260     * gets the lowest speed as a percentage of throttle that the loco reliably operates.
261     * @return percentage throttle
262     */
263    public float getMinReliableOperatingSpeed() {
264        return _minReliableOperatingSpeed;
265    }
266
267    /**
268     * Sets the lowest speed as a percentage of throttle that the loco reliably operates.
269     * @param speed percentage of throttle.
270     */
271    public void setMinReliableOperatingSpeed(float speed) {
272        _minReliableOperatingSpeed = speed;
273        if (_autoEngineer != null ) {
274            _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor);
275        }
276    }
277
278    /**
279     * @deprecated Use {@code ActiveTrain.setTrainDetection(TrainDetection value } insteadUse
280     * @param set True if entire train is detectable
281     */
282    @Deprecated (since="5.7.6",forRemoval=true)
283    public void setResistanceWheels(boolean set) {
284        if (set) {
285            _activeTrain.setTrainDetection(TrainDetection.TRAINDETECTION_WHOLETRAIN);
286        } else {
287            _activeTrain.setTrainDetection(TrainDetection.TRAINDETECTION_HEADONLY);
288        }
289    }
290
291    public boolean getRunInReverse() {
292        return _runInReverse;
293    }
294
295    public void setRunInReverse(boolean set) {
296        _runInReverse = set;
297    }
298
299    public boolean getSoundDecoder() {
300        return _soundDecoder;
301    }
302
303    public void setSoundDecoder(boolean set) {
304        _soundDecoder = set;
305    }
306
307    /**
308     *
309     * @return train length in MM.
310     */
311    public long getMaxTrainLengthMM() {
312        return _MaxTrainLength;
313    }
314
315    /**
316     * Set Train length in Scale Meters
317     * @param length length of train in meterd
318     * @param scaleFactor as supplied by scale object
319     */
320    public void setMaxTrainLength(double length, double scaleFactor) {
321        _MaxTrainLength =  (long) (length * 1000.0 * scaleFactor);
322        log.trace("setMaxTrainLength[{}]",_MaxTrainLength);
323    }
324
325    public void setUseSpeedProfile(boolean tf) {
326        _useSpeedProfileRequested = tf;
327    }
328
329    public boolean getUseSpeedProfile() {
330        return _useSpeedProfileRequested;
331    }
332
333    public void setStopBySpeedProfile(boolean tf) {
334        _stopBySpeedProfile = tf;
335    }
336
337    public void setStopBySpeedProfileAdjust(float adjust) {
338        _stopBySpeedProfileAdjust = adjust;
339    }
340
341    public boolean getStopBySpeedProfile() {
342        return _stopBySpeedProfile;
343    }
344
345    public float getStopBySpeedProfileAdjust() {
346        return _stopBySpeedProfileAdjust;
347    }
348    /**
349     * Set the F-Number for the light
350     * @param value F-Number
351     */
352    public void setFunctionLight(int value) {
353        _functionLight = value;
354    }
355    /**
356     * Returns the F-Number for the light.
357     * @return F-Number
358     */
359    public int getFunctionLight() {
360        return _functionLight;
361    }
362    /**
363     * Set the F-Number for the Bell
364     * @param value F-Number
365     */
366    public void setFunctionBell(int value) {
367        _functionBell = value;
368    }
369    /**
370     * Returns the F-Number for the Bell.
371     * @return F-Number
372     */
373    public int getFunctionBell() {
374        return _functionBell;
375    }
376    /**
377     * Set the F-Number for the Horn
378     * @param value F-Number
379     */
380    public void setFunctionHorn(int value) {
381        _functionHorn = value;
382    }
383    /**
384     * Returns the F-Number for the Horn.
385     * @return F-Number
386     */
387    public int getFunctionHorn() {
388        return _functionHorn;
389    }
390
391    /**
392     * Get current Signal DisplayName.
393     * @return empty String if no signal, otherwise Display Name.
394     */
395    public String getCurrentSignal() {
396        if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD)
397            return  (_controllingSignal == null  ) ? "" : _controllingSignal.getDisplayName() ;
398        else
399            return (_controllingSignalMast == null  ) ? "" : _controllingSignalMast.getDisplayName();
400    }
401
402    /**
403     * Get current Signal UserName.
404     * @return empty String if no signal, otherwise UserName.
405     */
406    public String getCurrentSignalUserName() {
407        if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD)
408            return  ( _controllingSignal == null || _controllingSignal.getUserName() == null) ? "" : _controllingSignal.getUserName();
409        else
410            return ( _controllingSignalMast == null || _controllingSignalMast.getUserName() == null) ? "" : _controllingSignalMast.getUserName();
411    }
412
413    private RosterEntry re = null;
414    boolean useSpeedProfile = false;
415
416    /**
417     * Initialize new Auto Active Train or get a new throttle after WORKING Sets
418     * up the DCC address and initiates creation of a throttle to run the train.
419     *
420     * @return true if initialized; false otherwise
421     */
422    public boolean initialize() {
423        //clear all flags
424        _pausingActive = false;
425        _stoppingBySensor = false;
426        _stoppingByBlockOccupancy = false;
427        _stoppingUsingSpeedProfile = false;
428        // get the dispatcher
429        _dispatcher = InstanceManager.getDefault(DispatcherFrame.class);
430
431        // Sync "Use stop sensor" from ActiveTrain/TrainInfo (default true if absent)
432        // When Override stop sensor is checked in the UI, ActiveTrain/TrainInfo will have useStopSensor == false.
433        this._useStopSensor = _activeTrain.getUseStopSensor();
434
435        // DEBUG
436        log.debug("{}: useStopSensor at init - ActiveTrain={}, AutoActiveTrain={}",
437                _activeTrain.getTrainName(), _activeTrain.getUseStopSensor(), this._useStopSensor);
438
439        // get decoder address
440        try {
441            _address = Integer.parseInt(_activeTrain.getDccAddress());
442        } catch (NumberFormatException ex) {
443            log.warn("invalid dcc address '{}' for {}", _activeTrain.getDccAddress(), _activeTrain.getTrainName());
444            return false;
445        }
446        if ((_address < 1) || (_address > 9999)) {
447            log.warn("invalid dcc address '{}' for {}", _activeTrain.getDccAddress(), _activeTrain.getTrainName());
448            return false;
449        }
450        // request a throttle for automatic operation, throttle returned via callback below
451        useSpeedProfile = false;
452        boolean ok;
453        _dccAddress = new DccLocoAddress(
454                _address,!InstanceManager.throttleManagerInstance().canBeShortAddress(_address));
455        if (_activeTrain.getTrainSource() == ActiveTrain.ROSTER) {
456            if (_activeTrain.getRosterEntry() != null) {
457                re = _activeTrain.getRosterEntry();
458                ok = InstanceManager.throttleManagerInstance().requestThrottle(re, this, false);
459                if (_useSpeedProfileRequested) {
460                    if (re.getSpeedProfile() != null && re.getSpeedProfile().getProfileSize() > 0) {
461                        useSpeedProfile = true;
462                    }
463                }
464                log.debug("{}: requested roster entry '{}', address={}, use speed profile requested={} usespeedprofile set={}",
465                        _activeTrain.getTrainName(), re.getId(), _address, _useSpeedProfileRequested, useSpeedProfile);
466            } else {
467                ok = InstanceManager.throttleManagerInstance().requestThrottle(_dccAddress, this, false);
468                log.debug("{}: requested throttle address={}, roster entry not found", _activeTrain.getTrainName(), _address);
469            }
470        } else {
471            ok = InstanceManager.throttleManagerInstance().requestThrottle(_dccAddress, this, false);
472            log.debug("{}: requested throttle address={}", _activeTrain.getTrainName(), _address);
473        }
474        if (!ok) {
475            log.warn("Throttle for locomotive address {} could not be setup.", _address);
476            _activeTrain.setMode(ActiveTrain.DISPATCHED);
477            return false;
478        }
479        return true;
480    }
481
482    // Throttle feedback method - Initiates running AutoEngineer with the new throttle
483    @Override
484    public void notifyThrottleFound(DccThrottle t) {
485        _throttle = t;
486        if (_throttle == null) {
487            JmriJOptionPane.showMessageDialog(null, java.text.MessageFormat.format(Bundle.getMessage(
488                    "Error28"), new Object[]{_activeTrain.getTrainName()}), Bundle.getMessage("MessageTitle"),
489                    JmriJOptionPane.INFORMATION_MESSAGE);
490            log.warn("null throttle returned for train '{}' during automatic initialization.", _activeTrain.getTrainName());
491            _activeTrain.setMode(ActiveTrain.DISPATCHED);
492            return;
493        }
494        log.debug("{}: New AutoEngineer, address={}, length (mm)={}, factor={}, useSpeedProfile={}",
495                _activeTrain.getTrainName(),
496                _throttle.getLocoAddress(),
497                getMaxTrainLengthMM(), _speedFactor, useSpeedProfile);
498        // get off this thread ASAP, some throttles does not completely initialize
499        // until this thread finishes
500        jmri.util.ThreadingUtil.runOnLayoutDelayed(() -> {
501            if (_autoEngineer != null) {
502                log.error("Second Trottle for same loco[{}] - ignoring", _address);
503                // at least make sure its going the right way...
504                setEngineDirection();
505            } else {
506                _autoEngineer = new AutoEngineer(t, re);
507                _activeTrain.setMode(ActiveTrain.AUTOMATIC);
508                // set initial direction
509                setEngineDirection();
510                _autoEngineer.setRamping(_currentRampRate, _dispatcher.getFullRampTime(),
511                        _dispatcher.getMinThrottleInterval(), _currentRampRate);
512                _autoEngineer.setSpeedLimits(_minReliableOperatingSpeed, _maxSpeed, _speedFactor);
513            }
514            if (_resumingAutomatic) {
515                _resumingAutomatic = false;
516                _activeTrain.setStatus(ActiveTrain.RUNNING);
517                setupNewCurrentSignal(null, true);
518                // if no current signal use saved.
519                if (!isCurrentSignal()) {
520                    restoreSavedSpeedAndDirection();
521                } else {
522                    setSpeedBySignal();
523                }
524            } else if (_dispatcher.getAutoAllocate()) {
525                // starting for the first time with automatic allocation of
526                // Sections
527                // the last of 2 threads must call setSpeedBySignal
528                // if the other thread is incomplete _currentAllocated Section
529                // will be null
530                if (_currentAllocatedSection != null) {
531                    setSpeedBySignal();
532                }
533            }
534        }, 500);
535    }
536
537    protected DccThrottle getThrottle() {
538        return _throttle;
539    }
540
541    @Override
542    public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
543        log.error("Throttle request failed for {} because {}", address, reason);
544    }
545
546    /**
547     * No steal or share decisions made locally
548     * <p>
549     * {@inheritDoc}
550     */
551    @Override
552    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
553    }
554
555    // more operational variables
556    // private final ArrayList<AllocatedSection> _allocatedSectionList = new ArrayList<>();
557    private jmri.jmrit.display.layoutEditor.LayoutBlockManager _lbManager = null;
558    private AllocatedSection _lastAllocatedSection = null;
559
560    protected Section getLastAllocatedSection() {
561        Section as = _activeTrain.getLastAllocatedSection();
562        return as;
563    }
564
565    private boolean _initialized = false;
566    private Section _nextSection = null;                      // train has not reached this Section yet
567    private volatile AllocatedSection _currentAllocatedSection = null;    // head of the train is in this Section
568    private volatile AllocatedSection _previousAllocatedSection = null;   // previous Section - part of train could still be in this section
569    private SignalHead _controllingSignal = null;
570    private SignalMast _controllingSignalMast = null;
571    private SignalHead _controllingSignalPrev = null;
572    private SignalMast _controllingSignalMastPrev = null;
573    private PropertyChangeListener _conSignalListener = null;
574    private PropertyChangeListener _conSignalMastListener = null;
575    private Block _conSignalProtectedBlock = null;
576    private volatile Block _currentBlock = null;
577    private Block _nextBlock = null;
578    private volatile Block _previousBlock = null;
579    private boolean _stoppingBySensor = false;
580    private Sensor _stopSensor = null;
581    private PropertyChangeListener _stopSensorListener = null;
582    private Turnout _turnoutStateNeeded = null;
583    private PropertyChangeListener _turnoutStateListener = null;
584    private boolean _stoppingByBlockOccupancy = false;    // if true, stop when _stoppingBlock goes UNOCCUPIED
585    private boolean _stoppingUsingSpeedProfile = false;     // if true, using the speed profile against the roster entry to bring the loco to a stop in a specific distance
586    // Distance stop is armed (waiting to start at the section's first occupied block)
587    private boolean _distanceStopPending = false;
588    // If true, the pending distance stop is an approach-to-min (hold until sensor), not a stop-to-zero
589    private boolean _distanceStopPendingToMin = false;
590    private float _distanceStopPendingMm = 0.0f;
591    private int _distanceStopPendingTask = NO_TASK;
592    private volatile Block _stoppingBlock = null;
593    private boolean _resumingAutomatic = false;  // if true, resuming automatic mode after WORKING session
594    private boolean _needSetSpeed = false;  // if true, train will set speed according to signal instead of stopping
595    private boolean waitingOnAllocation = false; //if true the train was stopped due to next section not allocated
596    // keeps track of and restores previous speed
597    private float _savedSpeed = 0.0f;
598    private boolean _savedForward = true;
599
600    public void set_useStopSensor(boolean _useStopSensor) {
601        this._useStopSensor = _useStopSensor;
602    }
603
604    private boolean _useStopSensor = true;                    //used by DispatcherSystem to override use of stop sensor
605
606    // --- Physics runtime state ---
607    private float _additionalWeightTonnes = 0.0f;      // extra consist mass in metric tonnes (t)
608    private float _rollingResistanceCoeff = 0.002f;    // dimensionless c_rr; default ~0.002
609
610    public void setAdditionalTrainWeightMetricTonnes(float tonnes) {
611        _additionalWeightTonnes = Math.max(0.0f, tonnes);
612    }
613    public float getAdditionalTrainWeightMetricTonnes() { return _additionalWeightTonnes; }
614
615    public void setRollingResistanceCoeff(float value) {
616        _rollingResistanceCoeff = Math.max(0.0f, value);
617    }
618    public float getRollingResistanceCoeff() { return _rollingResistanceCoeff; }
619
620    // Driver’s applied power/regulator during acceleration (0.0..1.0); default 1.0
621    private float _driverPowerPercent = 1.0f;
622    public void setDriverPowerPercent(float value) {
623        if (value < 0.0f) {
624            value = 0.0f;
625        }
626        if (value > 1.0f) {
627            value = 1.0f;
628        }
629        _driverPowerPercent = value;
630    }
631    public float getDriverPowerPercent() { return _driverPowerPercent; }
632
633    protected void saveSpeedAndDirection() {
634        _savedSpeed = _autoEngineer.getTargetSpeed();
635        _savedForward = _autoEngineer.getIsForward();
636    }
637
638    protected void restoreSavedSpeedAndDirection() {
639        _autoEngineer.setTargetSpeed(_savedSpeed);
640        _autoEngineer.setIsForward(_savedForward);
641    }
642
643    // keeps track of number of horn execution threads that are active
644    private int _activeHornThreads = 0;
645
646    protected void decrementHornExecution() {
647        _activeHornThreads--;
648    }
649
650    protected void incrementHornExecution() {
651        _activeHornThreads++;
652    }
653
654    //
655    // Notification methods
656    //
657    /**
658     * Handle notification of changes in section state.
659     *
660     * @param as the allocated that changed
661     */
662    protected void handleSectionStateChange(AllocatedSection as) {
663        if (!_activeTrain.isInAllocatedList(as)) {
664            addAllocatedSection(as);
665        }
666    }
667
668    /**
669     * Handle notification of allocation added to the ActiveTrain allocatedsections table.
670     * Subtly different from change in a sections status.
671     *
672     * @param evt the allocation that changed
673     */
674    private void handleAnotherSectionAllocatedChange( PropertyChangeEvent evt) {
675        if (waitingOnAllocation || _activeTrain.getSignalType() == DispatcherFrame.SECTIONSALLOCATED) {
676            waitingOnAllocation = false;
677            setSpeedBySignal();
678        }
679    }
680
681    /**
682     * Handle notification of changes in section occupancy.
683     *
684     * @param as the section that changed
685     */
686    protected void handleSectionOccupancyChange(AllocatedSection as) {
687        if (!_activeTrain.isInAllocatedList(as)) {
688            log.debug("Unexpected occupancy change notification - Section {}", as.getSection().getDisplayName(USERSYS));
689            return;
690        }
691        if (as.getSection().getOccupancy() == Section.OCCUPIED) {
692            // Section changed to OCCUPIED - process if expected next Section
693            if (as.getSection() == _nextSection) {
694                setNewCurrentSection(as);
695            }
696        } else if (as.getSection().getOccupancy() == Section.UNOCCUPIED) {
697            jmri.TransitSection ts = as.getTransitSection();
698            if (ts != null) {
699                _autoTrainAction.removeTransitSection(ts);
700            }
701        }
702    }
703
704    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC",
705            justification = "OK to not sync here, no conflict expected")
706    protected void handleBlockStateChange(AllocatedSection as, Block b) {
707        //Block oldPreviousBlock = _previousBlock;
708        if (b.getState() == Block.OCCUPIED) {
709            // Block changed to OCCUPIED - train has entered this block
710            log.debug("{}: handleBlockStateChange to OCCUPIED section {}, block {}, length {}", _activeTrain.getTrainName(),
711                    as.getSection().getDisplayName(USERSYS),
712                    b.getDisplayName(USERSYS), getBlockLength(b));
713            // If a distance stop is pending, start exactly at the first block INSIDE the current section
714            if (_distanceStopPending && _currentAllocatedSection != null) {
715                Block enter = _currentAllocatedSection.getEnterBlock(_previousAllocatedSection);
716                if (enter == b) {
717                    float mm = _distanceStopPendingMm;
718                    int taskPending = _distanceStopPendingTask;
719                    boolean toMin = _distanceStopPendingToMin;
720                    _distanceStopPending = false;
721                    _distanceStopPendingToMin = false;
722
723                    _stoppingUsingSpeedProfile = true; // commit to distance-based braking
724                    if (toMin) {
725                        // COMBINED (approach-to-min + sensor):
726                        re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval());
727                        re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval());
728                        re.getSpeedProfile().setMinCommandIntervalMs(_dispatcher.getMinThrottleInterval());
729                        re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed,
730                                _maxSpeedScaleKmh, (float) _dispatcher.getScale().getScaleRatio(),
731                                _autoEngineer.getIsForward());
732                        re.getSpeedProfile().planApproachToMinOverDistanceThenStopBySensor(
733                                getThrottle(), mm, _stopSensor, _speedFactor);
734                        return; // bypass legacy inner controller
735                    }
736                    // PURE distance stop-to-zero: cancel and plan via RosterSpeedProfile (same semantics)
737                    cancelStopInCurrentSection();
738                    re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, _maxSpeedScaleKmh,
739                            (float) _dispatcher.getScale().getScaleRatio(), _autoEngineer.getIsForward());
740                    re.getSpeedProfile().planStopToZeroOverDistance(getThrottle(), mm, _speedFactor);
741                    Thread tWait = jmri.util.ThreadingUtil.newThread(new WaitForTrainToStop(taskPending),
742                            "Wait for stop " + getActiveTrain().getActiveTrainName());
743                    tWait.start();
744                }
745            }
746            if (b == _nextBlock || _nextBlock == null) {
747                _currentBlock = b;
748                // defer setting the next/previous blocks until we know if its required and in what fashion
749                // for stopping blocks that action happens after the train has stopped.
750                // first check for entering the end point
751                if (!_activeTrain.isTransitReversed() && as.getSequence() == _activeTrain.getEndBlockSectionSequenceNumber()) {
752                    // are we going to reverse at end
753                    if ( _activeTrain.getReverseAtEnd() ) {
754                        removeCurrentSignal();
755                        stopInCurrentSection(END_REVERSAL, StopContext.DESTINATION);
756                    }
757                    // are we going continuously without delay
758                    else if ( _activeTrain.getResetWhenDone() && _activeTrain.getDelayedRestart() == ActiveTrain.NODELAY) {
759                        _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(),
760                                _activeTrain.getRestartSensor(),_activeTrain.getResetRestartSensor());
761                        _activeTrain.setTransitReversed(false);
762                        _activeTrain.resetAllAllocatedSections();
763                        _previousBlock = null;
764                        _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection);
765                        setEngineDirection();
766                        if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) {
767                            // we need to get a next section
768                            _dispatcher.queueScanOfAllocationRequests();
769                            // and then set the signal
770                        }
771                        // can be mid block
772                        setupNewCurrentSignal(null, true);
773                        setSpeedBySignal();
774                    }
775                    // are we restarting later
776                    else if ( _activeTrain.getResetWhenDone()) {
777                        // We enter this code for each block in the section.
778                        // If we stop in the farthest block eg Block 3 in a 3 Block Section
779                        // nothing special is required when starting.
780                        // If we stop in Block 1 of a 3 block section, and enter this code
781                        // when starting off again, so its just an advance of the _nextBlock.
782                        // we can tell which situation it is by looking
783                        // whether the _nextSection is not null and allocated to us.
784                        if ( _nextSection == null || !_activeTrain.isInAllocatedList(_nextSection)) {
785                            removeCurrentSignal();
786                            _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection);
787                            stopInCurrentSection(BEGINNING_RESET, StopContext.DESTINATION);
788                        } else {
789                            _nextBlock = getNextBlock(_currentBlock, _currentAllocatedSection);
790                        }
791                    }
792                    // else we are ending here
793                    else {
794                        log.debug("{}: Trip end, stop in Current Section, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS));
795                        removeCurrentSignal();
796                        stopInCurrentSection(END_TRAIN, StopContext.DESTINATION);
797                    }
798                }
799                // are we entering the start point
800                else if (_activeTrain.isTransitReversed() && as.getSequence() == _activeTrain.getStartBlockSectionSequenceNumber()) {
801                    // are we coming back from a reverse and running continiuosly
802                    if ( _activeTrain.getResetWhenDone() && _activeTrain.isTransitReversed() ) {
803                        removeCurrentSignal();
804                        stopInCurrentSection(BEGINNING_RESET, StopContext.DESTINATION);
805                    }
806                    // else we are ending here
807                    else {
808                        log.debug("{}: Trip end, stop in Current Section, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS));
809                        removeCurrentSignal();
810                        stopInCurrentSection(END_TRAIN, StopContext.DESTINATION);
811                    }
812                } else {
813                    // if we are not in first and not in last get the next block
814                    //_previousBlock = oldPreviousBlock;
815                    _nextBlock = getNextBlock(b, as);
816                    if (_nextBlock != null) {
817                        // this is a normal block/block change
818                        // set the blocks as normal
819                        _previousBlock = _currentBlock;
820                        _nextBlock = getNextBlock(b, as);
821                        //if (_nextBlock.getState() == Block.OCCUPIED) {
822                        //    handleBlockStateChange(as, _nextBlock);
823                        //}
824                        setupNewCurrentSignal(as, false);
825                    } else {
826                        // assume we have reached last block in this transit, for safety sake.
827                        log.warn("{}: No next Block from Block= {} Section= {}", _activeTrain.getTrainName(),
828                                b.getDisplayName(USERSYS), as.getSection().getDisplayName(USERSYS));
829                        removeCurrentSignal();
830                        stopInCurrentSection(NO_TASK);
831                    }
832                }
833            } else if (b != _currentBlock) {
834                log.trace("{}: block going occupied {} is not _nextBlock or _currentBlock - ignored.",
835                        _activeTrain.getTrainName(), b.getDisplayName(USERSYS));
836                return;
837            }
838        } else if (b.getState() == Block.UNOCCUPIED) {
839            log.debug("{}: handleBlockStateChange to UNOCCUPIED - Section {}, Block {}, speed {}", _activeTrain.getTrainName(),
840                    as.getSection().getDisplayName(USERSYS), b.getDisplayName(USERSYS),
841                    _autoEngineer == null ? "" : getTargetSpeed());
842            if (_stoppingByBlockOccupancy && (b == _stoppingBlock)) {
843                log.trace("{}: setStopNow by block occupancy from Block unoccupied, Block= {}", _activeTrain.getTrainName(), b.getDisplayName(USERSYS));
844                _stoppingByBlockOccupancy = false;
845                _stoppingBlock = null;
846                if (_needSetSpeed) {
847                    _needSetSpeed = false;
848                    setSpeedBySignal();
849                } else {
850                    setStopNow();
851                }
852            } else {
853                if (!isStopping() && _dispatcher.getUseOccupiedTrackSpeed()) {
854                    setSpeedBySignal();
855                }
856            }
857        }
858        _autoTrainAction.handleBlockStateChange(as, b);
859    }
860
861    /**
862     * support methods
863     */
864    protected void setEngineDirection() {
865        boolean oldFwd = getForward();
866        if (_runInReverse) {
867            setForward(_activeTrain.isTransitReversed());
868        } else {
869            setForward(!_activeTrain.isTransitReversed());
870        }
871        log.debug("[{}]flipping direction was [{}] now [{}]",_activeTrain.getActiveTrainName() ,oldFwd, getForward());
872    }
873
874    protected AllocatedSection getCurrentAllocatedSection() {
875        return _currentAllocatedSection;
876    }
877
878    /*
879     * Reverse lookup for allocated section.
880     */
881    protected AllocatedSection getAllocatedSectionForSection(Section s) {
882        for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) {
883            if (allocatedSection.getSection() == s)
884                return allocatedSection;
885        }
886        return null;
887    }
888
889    protected void allocateAFresh() {
890        //Reset initialized flag
891        _initialized = false;
892        // set direction
893        _currentAllocatedSection=null;
894        _currentBlock=null;
895        setForward(!getRunInReverse());
896    }
897
898    private void addAllocatedSection(AllocatedSection as) {
899        if (!_initialized) {
900            // this is first allocated section, get things started
901            _initialized = true;
902            _nextSection = as.getSection();
903            _currentBlock = _activeTrain.getStartBlock();
904            if (as.getSection().containsBlock(_currentBlock)) {
905                // starting Block is in this allocated section - find next Block
906                setNewCurrentSection(as);
907                _nextBlock = getNextBlock(_currentBlock, as);
908            } else if (as.getSection().connectsToBlock(_currentBlock)) {
909                // starting Block is connected to a Block in this allocated section
910                EntryPoint ep = as.getSection().getEntryPointFromBlock(_currentBlock, as.getDirection());
911                if (ep != null) {
912                    _nextBlock = ep.getBlock();
913                } else {
914                    log.error("failure to get entry point to Transit from Block {}", _currentBlock.getDisplayName(USERSYS));
915                }
916            }
917            if (_nextBlock != null) {
918                // set up new current signal, as this a beginning we allow a signal not at end of block
919                // to control the speed.
920                setupNewCurrentSignal(as,true);
921            }
922        }
923        // if train is stopping for lack of an allocation, set flag to restart it
924        if (!_pausingActive && (_lastAllocatedSection == _currentAllocatedSection)
925                && isStopping() && (_activeTrain.getStatus() == ActiveTrain.RUNNING)) {
926            _needSetSpeed = true;
927        }
928
929        // request next allocation if appropriate--Dispatcher must decide whether to allocate it and when
930        if ((!_dispatcher.getAutoAllocate()) && ((_lastAllocatedSection == null)
931                || (_lastAllocatedSection.getNextSection() == as.getSection()))) {
932            // if AutoAllocate, this is now done in DispatcherFrame.java for all trains
933            _lastAllocatedSection = as;
934            if (as.getNextSection() != null) {
935                Section nSection = as.getNextSection();
936                int nextSeq = as.getNextSectionSequence();
937                int nextDir = _activeTrain.getAllocationDirectionFromSectionAndSeq(nSection, nextSeq);
938                _dispatcher.requestAllocation(_activeTrain, nSection, nextDir, nextSeq, true, null);
939            }
940        }
941    }
942
943    private boolean isStopping() {
944        // here add indicator for new stopping methods, if any are added
945        return (_stoppingBySensor || _stoppingByBlockOccupancy || _stoppingUsingSpeedProfile);
946    }
947
948    private void removeCurrentSignal() {
949        if (_conSignalListener != null) {
950            _controllingSignal.removePropertyChangeListener(_conSignalListener);
951            _conSignalListener = null;
952        }
953        _controllingSignalPrev = _controllingSignal;
954        _controllingSignal = null;
955        if (_conSignalMastListener != null) {
956            _controllingSignalMast.removePropertyChangeListener(_conSignalMastListener);
957            _conSignalMastListener = null;
958        }
959        _controllingSignalMastPrev = _controllingSignalMast;
960        _controllingSignalMast = null;
961        _needSetSpeed = false;
962    }
963
964    /**
965     * checks for a controlling signal
966     * @return true if there is one
967     */
968    protected boolean isCurrentSignal() {
969        if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD)
970            return _controllingSignal != null;
971        else
972            // SignalMast
973            return _controllingSignalMast != null;
974    }
975
976    /**
977     *
978     * @param as current section the train is in, can be null
979     * @param forceSpeedChange if true, the speed will be set using the signal mast
980     *        even if it is not on the immediate block boundary
981     */
982    protected synchronized void setupNewCurrentSignal(AllocatedSection as, boolean forceSpeedChange) {
983        log.trace("setupNewCurrentSignal Called Section[{}] forceSpeedChange[{}]", as != null ? as.getSectionName() : "null",forceSpeedChange);
984        removeCurrentSignal();
985        if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD) {
986            SignalHead sh = _lbManager.getFacingSignalHead(_currentBlock, _nextBlock);
987            if (sh != null) {
988                _controllingSignal = sh;
989                _conSignalProtectedBlock = _nextBlock;
990                sh.addPropertyChangeListener(_conSignalListener = (PropertyChangeEvent e) -> {
991                    if (e.getPropertyName().equals("Appearance")) {
992                        // controlling signal has changed appearance
993                        setSpeedBySignal();
994                    }
995                });
996                _activeTrain.setControlingSignal(_controllingSignal, _controllingSignalPrev);
997                log.debug("new current signal = {}", sh.getDisplayName(USERSYS));
998            } else {
999                // Note: null signal head will result when exiting throat-to-throat blocks.
1000                log.warn("new current signal is null - sometimes OK");
1001            }
1002            setSpeedBySignal();
1003        } else if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST) {
1004            //SignalMast
1005            SignalMast sm = null;
1006            Block cB = _currentBlock;
1007            Block nB = _nextBlock;
1008            if (as == null) {
1009                as = _currentAllocatedSection;
1010            }
1011            // get signal mast at current block change, if there is no signal mast we will proceed with no change in speed
1012            // unless forceSpeedChange is true, such as beginning, resets of transit.
1013            // previous signal mast speed unless the mast is held.
1014            boolean weAreAtSpeedChangingMast=forceSpeedChange;
1015            if ( !forceSpeedChange  && nB != null ) {
1016                sm  = _lbManager.getFacingSignalMast(cB, nB);
1017                if (sm != null) {weAreAtSpeedChangingMast=true;}
1018            }
1019
1020            while (sm == null && nB != null) {
1021                sm = _lbManager.getFacingSignalMast(cB, nB);
1022                if (sm == null) {
1023                    cB = nB;
1024                    nB = getNextBlock(nB, as);
1025                }
1026            }
1027            if (sm != null) {
1028                _controllingSignalMast = sm;
1029                _conSignalProtectedBlock = nB;
1030                sm.addPropertyChangeListener(_conSignalMastListener = (PropertyChangeEvent e) -> {
1031                    if (e.getPropertyName().equals("Aspect") || e.getPropertyName().equals("Held")) {
1032                        // controlling signal has changed appearance or a hold has been released
1033                        // even if its a hold we still have to use target speed etc else we override pauses and other stop events.
1034                        setSpeedBySignal();
1035                    }
1036                });
1037                _activeTrain.setControlingSignal(_controllingSignalMast, _controllingSignalMastPrev);
1038                log.debug("{}: new current signalmast {}({}) for section {}", _activeTrain.getTrainName(), sm.getDisplayName(USERSYS),
1039                        sm.getAspect(), as.getSection().getDisplayName(USERSYS));
1040                if ( weAreAtSpeedChangingMast ) {
1041                    setSpeedBySignal();
1042                } else {
1043                    checkForGhost();
1044                }
1045            } else {
1046                // There is a missing signal mast at a block boundary.
1047                // If the next block is allocated to this train we can continue.
1048                // If the train was stopped here we can try and restart it. Either way we use
1049                // setting setSpeedBySectionsAllocated as a way out of the dilemma.
1050                log.debug("{}: new current signalmast is null for section {} - sometimes OK", _activeTrain.getTrainName(),
1051                        as == null ? "Null" : as.getSection().getDisplayName(USERSYS));
1052                if (_nextBlock == null || ! _activeTrain.getBlockList().contains(_nextBlock) ||  _autoEngineer.isStopped()) {
1053                    log.warn("{}: new current signalmast is null for section {} and next block is not this trains. Temporarily continuing by allocations", _activeTrain.getTrainName(),
1054                            as == null ? "Null" : as.getSection().getDisplayName(USERSYS));
1055                    setSpeedBySectionsAllocated();
1056                }
1057                checkForGhost();
1058            }
1059        } else {
1060            setSpeedBySignal();
1061        }
1062    }
1063
1064    @CheckForNull
1065    private Block getNextBlock(Block b, AllocatedSection as) {
1066        //if (((_currentBlock == _activeTrain.getEndBlock()) && _activeTrain.getReverseAtEnd()
1067        //        && (as.getSequence() == _activeTrain.getEndBlockSectionSequenceNumber()))) {
1068        //    return _previousBlock;
1069        //}
1070        if ((_currentBlock == _activeTrain.getStartBlock())
1071                && _activeTrain.getResetWhenDone() && _activeTrain.isTransitReversed()
1072                && (as.getSequence() == _activeTrain.getStartBlockSectionSequenceNumber()))
1073            return _previousBlock;
1074        if (as.getNextSection() != null) {
1075            EntryPoint ep = as.getSection().getExitPointToSection(_nextSection, as.getDirection());
1076            if ((ep != null) && (ep.getBlock() == b))
1077                // this block is connected to a block in the next section
1078                return ep.getFromBlock();
1079        }
1080        // this allocated section has multiple blocks _or_ there is no next Section
1081        Block blk = as.getSection().getEntryBlock();
1082        while (blk != null) {
1083            if (b == blk)
1084                return as.getSection().getNextBlock();
1085            blk = as.getSection().getNextBlock();
1086        }
1087        return null;
1088    }
1089
1090    private void setNewCurrentSection(AllocatedSection as) {
1091        if (as.getSection() == _nextSection) {
1092            _previousAllocatedSection = _currentAllocatedSection;
1093            _currentAllocatedSection = as;
1094            _nextSection = as.getNextSection();
1095            TransitSection ts = as.getTransitSection();
1096            if (ts != null) {
1097                _autoTrainAction.addTransitSection(ts);
1098            }
1099            // written the long way for readability
1100            boolean nextSectionExpected = true;
1101            if (ts != null &&
1102                    ts.isSafe() &&
1103                    _activeTrain.getAllocateMethod() == ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS) {
1104                nextSectionExpected = false;
1105            } else if (!_activeTrain.isAllocationReversed() &&
1106                    _activeTrain.getEndBlockSection() == _currentAllocatedSection.getSection()) {
1107                nextSectionExpected = false;
1108            } else if (_activeTrain.isAllocationReversed() &&
1109                    _activeTrain.getStartBlockSectionSequenceNumber() == _currentAllocatedSection.getSequence()) {
1110                nextSectionExpected = false;
1111            }
1112            log.debug("{}:Next Section Expected[{}]",_activeTrain.getActiveTrainName(),  nextSectionExpected);
1113            // NOw handled in SetSpeedBySignal()
1114            // check if new next Section exists but is not allocated to this train excepting above circumstances
1115            //if ( nextSectionExpected &&_nextSection != null && !_activeTrain.isInAllocatedList(_nextSection)) {
1116            //    // next section is not allocated to this train, must not enter it, even if signal is OK.
1117            //    log.warn("Stopping train [{}] in section [{}], as next section [{}] is not allocated",
1118            //            _activeTrain.getActiveTrainName(),_currentAllocatedSection.getSection().getDisplayName(USERSYS),_nextSection.getDisplayName(USERSYS));
1119            //    stopInCurrentSection(NO_TASK);
1120            //    _needSetSpeed = false;
1121            //}
1122            // see if we need to rescan as entering safe section.
1123            if (ts != null &&
1124                    ts.isSafe() &&
1125                    _activeTrain.getAllocateMethod() == ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS) {
1126                _dispatcher.queueScanOfAllocationRequests();
1127            }
1128
1129        }
1130    }
1131
1132    // Criteria for being able to set or get a speed.
1133    protected boolean canSpeedBeSetOrChecked() {
1134        if (_pausingActive || getAutoEngineer() == null ||
1135                ((_activeTrain.getStatus() != ActiveTrain.RUNNING) &&
1136                        (_activeTrain.getStatus() != ActiveTrain.WAITING)) ||
1137                !_activeTrain.getStarted() ||
1138                (_activeTrain.getMode() != ActiveTrain.AUTOMATIC)) {
1139            log.debug("{}:Train is not currently eligible for settingspeed or checking ghosts",_activeTrain.getActiveTrainName());
1140            return false;
1141        }
1142        return true;
1143    }
1144
1145    // called by above or when resuming after stopped action
1146    protected synchronized void setSpeedBySignal() {
1147        log.trace("Set Speed by Signal");
1148        if (!canSpeedBeSetOrChecked()) {
1149            log.trace("[{}]:cannot set speed.",getActiveTrain().getActiveTrainName());
1150            return;
1151        }
1152
1153        // only bother to check signal if the next allocation is ours.
1154        // and the turnouts have been set
1155        if (checkAllocationsAhead() && checkTurn(getAllocatedSectionForSection(_nextSection))) {
1156            if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALHEAD
1157                    && _controllingSignal != null) {
1158                setSpeedBySignalHead();
1159            } else if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST
1160                    && _controllingSignalMast != null) {
1161                setSpeedBySignalMast();
1162            } else {
1163                log.trace("{}:Set Speed by BlocksAllocated",_activeTrain.getActiveTrainName());
1164                setSpeedBySectionsAllocated();
1165            }
1166            checkForGhost();
1167        } else {
1168            // This might be the last section....
1169            if (_currentAllocatedSection != null && _currentAllocatedSection.getNextSection() == null) {
1170                stopInCurrentSection(END_TRAIN, StopContext.DESTINATION);
1171            } else {
1172                // This will stop it.
1173                stopInCurrentSection(NO_TASK);
1174                log.debug("{}:Set Stop",_activeTrain.getActiveTrainName());
1175                waitingOnAllocation = true;  // flag setSpeedBySignal required when another allocation made.
1176            }
1177        }
1178    }
1179
1180    private void checkForGhost() {
1181        if (!canSpeedBeSetOrChecked()) {
1182            log.trace("[{}]:cannot check for ghost.",getActiveTrain().getActiveTrainName());
1183            return;
1184        }
1185        if ( !(getTargetSpeed() == 0.0f || isStopping())
1186                && _nextBlock != null
1187                && _currentBlock != null
1188                && _nextBlock.getSensor() != null
1189                && _nextBlock.getIsGhost()) {
1190            if ( _currentBlock.getIsGhost()) {
1191                log.error("Stopping due to two consecutive no sensor blocks [{}], [{}]",
1192                        _currentBlock.getDisplayName(), _nextBlock.getDisplayName());
1193            } else {
1194                try {
1195                    _currentBlock.addPropertyChangeListener(new DarkTerritoryListener(_nextBlock.getSensor()));
1196                    _nextBlock.getSensor().setKnownState(Sensor.ACTIVE);
1197                } catch (jmri.JmriException ex) {
1198                    log.error("Error entering darkterratory");
1199                }
1200            }
1201        }
1202    }
1203
1204    /*
1205     * Check at least the next section is allocated
1206     */
1207    private boolean checkAllocationsAhead() {
1208        if (_nextSection != null) {
1209            // Check that next section is allocated...
1210            for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) {
1211                if (allocatedSection.getSection() == _nextSection)
1212                    return true;
1213            }
1214        }
1215        return false;
1216    }
1217
1218    private void setSpeedBySectionsAllocated() {
1219        if (!canSpeedBeSetOrChecked()) {
1220            log.trace("[{}]:cannot set speed.",getActiveTrain().getActiveTrainName());
1221            return;
1222        }
1223
1224        if (_stoppingByBlockOccupancy && (_stoppingBlock != null && _stoppingBlock.getState() == Block.UNOCCUPIED))
1225            // we are awaiting a delayed stop
1226            return;
1227        int sectionsAhead = 0;
1228        for (AllocatedSection allocatedSection : _activeTrain.getAllocatedSectionList()) {
1229            if (!allocatedSection.getEntered()) {
1230                sectionsAhead++;
1231            }
1232        }
1233        float newSpeed = 0.0f;
1234        log.debug("[{}:SectionsAhead[{}]",_activeTrain.getActiveTrainName() ,sectionsAhead);
1235        switch (sectionsAhead) {
1236            case 0:
1237                newSpeed = 0.0f;
1238                break;
1239            case 1:
1240                newSpeed = InstanceManager.getDefault(SignalSpeedMap.class)
1241                .getSpeed("Medium");
1242                // .getSpeed(_dispatcher.getStoppingSpeedName());
1243                _activeTrain.setStatus(ActiveTrain.RUNNING);
1244                break;
1245            default:
1246                newSpeed = InstanceManager.getDefault(SignalSpeedMap.class)
1247                .getSpeed("Normal");
1248                // .getSpeed(_dispatcher.getStoppingSpeedName());
1249                _activeTrain.setStatus(ActiveTrain.RUNNING);
1250        }
1251        if (_dispatcher.getUseOccupiedTrackSpeed()) {
1252            newSpeed = getMinSpeedOfOccupiedBlocks(newSpeed);
1253        }
1254        // see if needs to slow for next block.
1255        if (newSpeed > 0 && _nextBlock != null) {
1256            float speed = getSpeedFromBlock(_nextBlock);
1257            if (speed < newSpeed) {
1258                // slow for next block
1259                newSpeed = speed;
1260            }
1261        }
1262        if (newSpeed > 0) {
1263            log.trace("setSpeedBySectionsAllocated isStopping[{}]",isStopping());
1264            cancelStopInCurrentSection();
1265            setTargetSpeed(getThrottleSettingFromSpeed(newSpeed));
1266        } else {
1267            waitingOnAllocation = true;
1268            stopInCurrentSection(NO_TASK);
1269        }
1270    }
1271
1272    // Check for speed of incoming blocks.
1273    // in and out speed in is throttle percent.
1274    private float getMinSpeedOfOccupiedBlocks(float speed) {
1275        if (!_dispatcher.getUseOccupiedTrackSpeed())
1276            return speed;
1277        // get slowest speed of any entered and still occupied
1278        // or entered but not released (HEADONLY / HEADANDTAIL
1279        float newSpeed = speed;
1280        for (AllocatedSection asE : _activeTrain.getAllocatedSectionList()) {
1281            if (asE.getEntered()) {
1282                for (Block b : asE.getSection().getBlockList()) {
1283                    if (b.getState() == Block.OCCUPIED
1284                            || _activeTrain.getTrainDetection() != TrainDetection.TRAINDETECTION_WHOLETRAIN ) {
1285                        if (getSpeedFromBlock(b) < newSpeed) {
1286                            newSpeed = getSpeedFromBlock(b);
1287                        }
1288                    }
1289                }
1290            }
1291        }
1292        log.trace("{}: getMinSpeedOfOccupiedBlocks Org Speed [{}] New [{}]",
1293                _activeTrain.getActiveTrainName(), speed, newSpeed);
1294        return newSpeed;
1295    }
1296
1297    /**
1298     * Check that all turnouts in a section have finished setting
1299     * for passage. If not listens on first bad turnout
1300     * and rechecks when set.
1301     * @param as Allocated section whose turnouts need to be checked.
1302     * @return true if no errors else false
1303     */
1304    private boolean checkTurn(AllocatedSection as) {
1305        if (as != null && as.getAutoTurnoutsResponse() != null) {
1306            if (_turnoutStateNeeded  != null && _turnoutStateListener != null) {
1307                _turnoutStateNeeded.removePropertyChangeListener("KnownState",_turnoutStateListener);
1308                _turnoutStateNeeded = null;
1309                _turnoutStateListener =null;
1310            }
1311            _turnoutStateNeeded = _dispatcher.getAutoTurnoutsHelper().checkStateAgainstList(as.getAutoTurnoutsResponse());
1312            if (_turnoutStateNeeded != null) {
1313                _turnoutStateNeeded.addPropertyChangeListener("KnownState",_turnoutStateListener = (PropertyChangeEvent e) -> {
1314                    _turnoutStateNeeded.removePropertyChangeListener("KnownState",_turnoutStateListener);
1315                    _turnoutStateListener=null;
1316                    _turnoutStateNeeded=null;
1317                    setSpeedBySignal();
1318                });
1319                return false;
1320            }
1321        }
1322        return true;
1323    }
1324
1325    private void setSpeedBySignalMast() {
1326        //Set speed using SignalMasts;
1327        if (_controllingSignalMast == null) {
1328            // temporarily revert to by sections allocated
1329            setSpeedBySectionsAllocated();
1330            return;
1331        }
1332        String displayedAspect = _controllingSignalMast.getAspect();
1333        if (log.isTraceEnabled()) {
1334            log.trace("{}: Controlling mast {} ({})", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), displayedAspect);
1335            if (_conSignalProtectedBlock == null) {
1336                log.trace("{}: Protected block is null", _activeTrain.getTrainName());
1337            } else {
1338                log.trace("{}: Protected block: {} state: {} speed: {}", _activeTrain.getTrainName(),
1339                        _conSignalProtectedBlock.getSensor().getDisplayName(USERSYS),
1340                        (_conSignalProtectedBlock.getSensor().getState() == Block.OCCUPIED ? "OCCUPIED" : "NOT OCCUPIED"),
1341                        _conSignalProtectedBlock.getBlockSpeed());
1342            }
1343        }
1344
1345        if ((_controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.DANGER).equals(displayedAspect))
1346                || !_controllingSignalMast.getLit() || _controllingSignalMast.getHeld()) {
1347            checkForSignalPassedOrStop(_controllingSignalMast.getDisplayName(USERSYS));
1348        } else if (_controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.PERMISSIVE) != null
1349                && _controllingSignalMast.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.PERMISSIVE).equals(displayedAspect)) {
1350            setTargetSpeedState(RESTRICTED_SPEED);
1351            _activeTrain.setStatus(ActiveTrain.RUNNING);
1352        } else {
1353
1354            //if using signalmasts, set speed to lesser of aspect speed and signalmastlogic speed
1355            //  (minimum speed on the path to next signal, using turnout and block speeds)
1356            String aspectSpeedStr = (String) _controllingSignalMast.getSignalSystem().getProperty(displayedAspect, "speed");
1357            log.trace("{}: Signal {} speed {} for aspect {}", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), aspectSpeedStr, displayedAspect);
1358            float speed = -1.0f;
1359            if (aspectSpeedStr != null) {
1360                try {
1361                    speed = Float.parseFloat(aspectSpeedStr);
1362                } catch (NumberFormatException nx) {
1363                    try {
1364                        speed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(aspectSpeedStr);
1365                        log.trace("{}: Signal {} speed from map for {} is {}", _activeTrain.getTrainName(), _controllingSignalMast.getDisplayName(USERSYS), aspectSpeedStr, speed);
1366                    } catch (IllegalArgumentException ex) {
1367                        //Considered Normal if the speed does not appear in the map
1368                        log.trace("{}: Speed not found {}", _activeTrain.getTrainName(), aspectSpeedStr);
1369                    }
1370                }
1371            }
1372            int aspectSpeed = (int) speed; //save for debug message
1373
1374            //get maximum speed for the route between current and next signalmasts
1375            float smLogicSpeed = -1.0f;
1376            String smDestinationName = "unknown";
1377            SignalMastLogic smLogic = InstanceManager.getDefault(SignalMastLogicManager.class).getSignalMastLogic(_controllingSignalMast);
1378            if (smLogic != null) {
1379                SignalMast smDestination = smLogic.getActiveDestination();
1380                if (smDestination != null) {
1381                    smDestinationName = smDestination.getDisplayName(USERSYS);
1382                    smLogicSpeed = (int) smLogic.getMaximumSpeed(smDestination);
1383                }
1384            }
1385
1386            //use the smaller of aspect speed or route speed
1387            if (smLogicSpeed > -1.0f && smLogicSpeed < speed) {
1388                speed = smLogicSpeed;
1389            }
1390
1391            log.debug("{}: {}({}) {}({}), Dest: {}, path max: {}",
1392                    _activeTrain.getTrainName(),
1393                    _controllingSignalMast.getDisplayName(USERSYS), displayedAspect, aspectSpeedStr, aspectSpeed,
1394                    smDestinationName, (int) smLogicSpeed);
1395            // Adjust for occupied blocks.
1396            if (_dispatcher.getUseOccupiedTrackSpeed()) {
1397                speed = getMinSpeedOfOccupiedBlocks(speed);
1398            }
1399            if (speed > -1.0f) {
1400                /* We should work on the basis that the speed required in the current block/section is governed by the signalmast
1401                 that we have passed and not the one we are approaching when we are accelerating.
1402                 However when we are decelerating we should be aiming to meet the speed required by the approaching signalmast
1403                 whether that is to slow down or come to a complete stand still.
1404                 */
1405                if (prevSpeed == -1 || speed < prevSpeed) {
1406                    log.debug("{}: Signal {} setting speed to {} for next", _activeTrain.getTrainName(),
1407                            _controllingSignalMast.getDisplayName(USERSYS), speed);
1408                    setTargetSpeedValue(speed);
1409                } else {
1410                    log.debug("{}: Signal {} setting speed to {} for previous", _activeTrain.getTrainName(),
1411                            _controllingSignalMast.getDisplayName(USERSYS), speed);
1412                    setTargetSpeedValue(prevSpeed);
1413                }
1414                prevSpeed = speed;
1415                _activeTrain.setStatus(ActiveTrain.RUNNING);
1416
1417            } else {
1418                log.warn("{}: No specific speeds found so will use the default", _activeTrain.getTrainName());
1419                setTargetSpeedState(NORMAL_SPEED);
1420                _activeTrain.setStatus(ActiveTrain.RUNNING);
1421            }
1422        }
1423    }
1424
1425    private void setSpeedBySignalHead() {
1426        // a held signal always stop
1427        if ( _controllingSignal != null && _controllingSignal.getAppearance() == SignalHead.HELD ) {
1428            // Held - Stop
1429            stopInCurrentSection(NO_TASK, StopContext.SIGNAL);
1430            return;
1431        }
1432
1433        if (useSpeedProfile) {
1434            // find speed from signal.
1435            // find speed from block
1436            // use least
1437            float blockSpeed = getSpeedFromBlock(_conSignalProtectedBlock);
1438
1439            float signalSpeed;
1440            String signalSpeedName;
1441            String displayedAspect = _controllingSignal.getAppearanceName();
1442            try {
1443                signalSpeedName =
1444                        InstanceManager.getDefault(SignalSpeedMap.class).getAppearanceSpeed(displayedAspect);
1445                signalSpeed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(signalSpeedName);
1446            } catch (Throwable ex) { // if _anything_ goes wrong, contain it
1447                signalSpeed = -1.0f;
1448                log.warn("{}: Block {} AppearanceSpeed {} not found in SignalSpeedMap",
1449                        _activeTrain.getTrainName(), _conSignalProtectedBlock.getDisplayName(USERSYS), displayedAspect);
1450            }
1451            float useSpeed;
1452            if (blockSpeed < signalSpeed) {
1453                useSpeed = blockSpeed;
1454            } else {
1455                useSpeed = signalSpeed;
1456            }
1457
1458            log.trace("BlockSpeed[{}] SignalSpeed[{}]", blockSpeed, signalSpeed);
1459            if (useSpeed < 0.01f) {
1460                checkForSignalPassedOrStop(_controllingSignal.getDisplayName(USERSYS));
1461            } else {
1462                setTargetSpeedByProfile(useSpeed,_stopBySpeedProfileAdjust,true);
1463            }
1464        } else {
1465            switch (_controllingSignal.getAppearance()) {
1466                case SignalHead.DARK:
1467                case SignalHead.RED:
1468                case SignalHead.FLASHRED:
1469                    // May get here from signal changing before Block knows it is occupied, so must
1470                    //      check Block occupancy sensor, which must change before signal.
1471                    // check to to see if its allocated to us!!!
1472                    //      check Block occupancy sensor if it is in an allocated block, which must change before signal
1473                    // If the train has no _currentAllocatedSection it is in a first block outside transit.
1474                    checkForSignalPassedOrStop(_controllingSignal.getDisplayName(USERSYS));
1475                    break;
1476                case SignalHead.YELLOW:
1477                case SignalHead.FLASHYELLOW:
1478                    setTargetSpeedState(SLOW_SPEED);
1479                    _activeTrain.setStatus(ActiveTrain.RUNNING);
1480                    break;
1481                case SignalHead.GREEN:
1482                case SignalHead.FLASHGREEN:
1483                    setTargetSpeedState(NORMAL_SPEED);
1484                    _activeTrain.setStatus(ActiveTrain.RUNNING);
1485                    break;
1486                case SignalHead.LUNAR:
1487                case SignalHead.FLASHLUNAR:
1488                    setTargetSpeedState(RESTRICTED_SPEED);
1489                    _activeTrain.setStatus(ActiveTrain.RUNNING);
1490                    break;
1491                default:
1492                    log.warn("Signal Head[{}] has invalid Appearence - using stop",_controllingSignal.getAppearance());
1493                    stopInCurrentSection(NO_TASK, StopContext.SIGNAL);
1494            }
1495
1496        }
1497    }
1498
1499    /**
1500     * Check to see if a stop is really required, or if this is the
1501     * signal head that was just passed, in which case ignore as the signal goes red before a
1502     * new signal exists.
1503     *
1504     * @param displayName name of signal for debug messages.
1505     */
1506    private void checkForSignalPassedOrStop(String displayName) {
1507        // if current section is null we are in a pre transit block.
1508        if (_currentAllocatedSection != null) {
1509            if ((_currentAllocatedSection.isInActiveBlockList(_conSignalProtectedBlock) ||
1510                    (_nextSection != null && _activeTrain.isInAllocatedList(_nextSection) && _nextSection.containsBlock(_conSignalProtectedBlock)))
1511                    && _conSignalProtectedBlock.getSensor().getState() == Block.OCCUPIED) {
1512                // Train has just passed this signal - ignore this signal
1513                log.debug("{}: _conSignalProtectedBlock [{}] for signal [{}] is the block just past so ignore.", _activeTrain.getTrainName(),
1514                        _conSignalProtectedBlock.getDisplayName(USERSYS), displayName);
1515            } else {
1516                log.debug("{}: stopping for signal [{}] ", _activeTrain.getTrainName(),
1517                        displayName);
1518                stopInCurrentSection(NO_TASK);
1519            }
1520        }
1521    }
1522
1523    protected float getSpeedFromBlock(Block block) {
1524        String blockSpeedName = block.getBlockSpeed();
1525        if (blockSpeedName.contains("Global")) {
1526            blockSpeedName = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed();
1527        }
1528        float blockSpeed = -1.0f;
1529        if (!blockSpeedName.isEmpty()) {
1530            try {
1531                blockSpeed = Float.parseFloat(blockSpeedName);
1532            } catch (NumberFormatException nx) {
1533                try {
1534                    blockSpeed = InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(blockSpeedName);
1535                    log.debug("{} {}: block speed from map for {} is {}",
1536                            _activeTrain.getTrainName(), block.getDisplayName(USERSYS), blockSpeedName,
1537                            blockSpeed);
1538                } catch (Throwable ex) { // if _anything_ goes wrong, contain it
1539                    //Considered Normal if the speed does not appear in the map
1540                    log.warn("{}: Block {} Speed {} not found in SignalSpeedMap",
1541                            _activeTrain.getTrainName(), block.getDisplayName(USERSYS), blockSpeed);
1542                }
1543            }
1544        }
1545        return blockSpeed;
1546    }
1547
1548    float prevSpeed = -1.0f;
1549
1550    // called to cancel a stopping action that is in progress
1551    private synchronized void cancelStopInCurrentSection() {
1552        log.trace("[{}]:Cancel Stopping", _activeTrain.getTrainName());
1553        // Cancel any pending or in-progress stop-by-distance / profile-based stopping schedules.
1554        // If conditions improve (signal clears or allocations become available), we must be able to cancel
1555        // the stopping sequence and resume running.
1556        _distanceStopPending = false;
1557        _distanceStopPendingToMin = false;
1558        _distanceStopPendingMm = 0.0f;
1559        _distanceStopPendingTask = NO_TASK;
1560        if (re != null && re.getSpeedProfile() != null) {
1561            try {
1562                re.getSpeedProfile().cancelSpeedChange();
1563            } catch (RuntimeException ex) {
1564                log.warn("{}: cancelSpeedChange failed while cancelling stop", _activeTrain.getTrainName(), ex);
1565            }
1566        }
1567
1568        cancelStoppingBySensor();
1569        _stoppingByBlockOccupancy = false;
1570        _stoppingBlock = null;
1571        _stoppingUsingSpeedProfile = false;
1572        if (_autoEngineer != null) {
1573            _autoEngineer.slowToStop(false);
1574        }
1575    }
1576
1577    /** Clamp throttle [% 0..1] */
1578    private static float clampThrottle(float pct) {
1579        if (pct < 0.0f) return 0.0f;
1580        if (pct > 1.0f) return 1.0f;
1581        return pct;
1582    }
1583
1584    private enum StopContext {
1585        DESTINATION,
1586        SIGNAL,
1587        OTHER
1588    }
1589
1590    private synchronized void stopInCurrentSection(int task) {
1591        stopInCurrentSection(task, StopContext.OTHER);
1592    }
1593
1594    private synchronized void stopInCurrentSection(int task, StopContext context) {
1595        if (_currentAllocatedSection == null) {
1596            log.error("{}: Current allocated section null on entry to stopInCurrentSection", _activeTrain.getTrainName());
1597            setStopNow();
1598            return;
1599        }
1600
1601        log.debug("{}: StopInCurrentSection called for {} task[{}] targetspeed[{}]", _activeTrain.getTrainName(),
1602                _currentAllocatedSection.getSection().getDisplayName(USERSYS), task, getTargetSpeed());
1603
1604        if (((_autoEngineer != null) && _autoEngineer.isStopped()) || isStopping()) {
1605            log.debug("{}: train is already stopped or stopping.", _activeTrain.getTrainName());
1606            // ignore if train is already stopped or if stopping is in progress
1607            return;
1608        }
1609
1610
1611        /* =======================================================================
1612         * Distance-based stopping (destination section only) — custom planner.
1613         * We compute a constant-deceleration braking curve to stop exactly at 'distanceMm'
1614         * and drive the throttle ourselves via AutoEngineer.setSpeedImmediate(...).
1615         *
1616         * No dependency on RosterSpeedProfile.changeLocoSpeed or AutoEngineer.setTargetSpeed(distance,...).
1617         * We only read profile speeds via re.getSpeedProfile().getDistanceTravelled(...) to invert throttle ↔ mm/s.
1618         *
1619         * TODO (future): extend to signal stop points inside sections using the same controller,
1620         * with an explicit per-section stop origin.
1621         * ======================================================================= */
1622        // Distance-based stopping is currently applied only to destination/platform-style stops.
1623        // For signal-driven and other stops, preserve existing Dispatcher stop behavior.
1624        boolean allowDistanceStop = (context == StopContext.DESTINATION);
1625
1626        if (allowDistanceStop) {
1627            boolean distanceEnabled = (_stopByDistanceMm > 0.0f);
1628        // Direction-aware profile availability (we must have speeds for the current direction)
1629        boolean profileAvailable = false;
1630        if (re != null && re.getSpeedProfile() != null) {
1631            boolean forward = _autoEngineer.getIsForward();
1632            profileAvailable = forward ? re.getSpeedProfile().hasForwardSpeeds()
1633                    : re.getSpeedProfile().hasReverseSpeeds();
1634        }
1635
1636        // Resolve the section's stopping sensor for the current travel direction (do not mutate _stopSensor yet)
1637        Sensor stopSensorCandidate = null;
1638        if (_currentAllocatedSection != null) {
1639            if (_currentAllocatedSection.getSection().getState() == Section.FORWARD) {
1640                stopSensorCandidate = _currentAllocatedSection.getSection().getForwardStoppingSensor();
1641            } else {
1642                stopSensorCandidate = _currentAllocatedSection.getSection().getReverseStoppingSensor();
1643            }
1644        }
1645
1646        // Refresh override flag in case it changed (e.g., user updated the Train Info while running)
1647        this._useStopSensor = _activeTrain.getUseStopSensor();
1648
1649        // Combined mode = user opted into Stop-by-Distance, profile is available, and a stopping sensor is present & in use
1650        boolean combinedMode = distanceEnabled && profileAvailable && (_useStopSensor) && (stopSensorCandidate != null);
1651
1652        // DEBUG
1653        log.debug("{}: stopInSection - distEnabled={}, profileAvail={}, sensorPresent={}, useStopSensor={}, combined={}",
1654                _activeTrain.getTrainName(), distanceEnabled, profileAvailable, (stopSensorCandidate != null), _useStopSensor, combinedMode);
1655
1656        if ((distanceEnabled && profileAvailable) && !_stoppingUsingSpeedProfile && !_distanceStopPending) {
1657
1658            // Compute requested travel distance from section entry to stop reference
1659
1660            float distanceMmBase = _stopByDistanceMm + (_stopByDistanceRefTail ? getMaxTrainLengthMM() : 0.0f);
1661            // Safety: do not allow stop-by-distance to extend beyond the destination section.
1662            // This prevents overrunning into the next section / train ahead when a large distance is configured.
1663            float sectionLenMm = (_currentAllocatedSection != null) ? _currentAllocatedSection.getActualLength() : 0.0f;
1664            if (sectionLenMm > 0.0f && distanceMmBase > sectionLenMm) {
1665                log.warn("{}: stop-by-distance {}mm exceeds section length {}mm; clamping to section length.",
1666                        _activeTrain.getTrainName(), Float.valueOf(distanceMmBase), Float.valueOf(sectionLenMm));
1667                distanceMmBase = sectionLenMm;
1668            }
1669
1670            if (combinedMode) {
1671                // --- New combined behaviour ---
1672                // We will decelerate to MinimumReliableOperatingSpeed within distanceMmBase, then hold until the stop sensor fires.
1673
1674                // Decide whether to start NOW (already past the section entry) or ARM to start at the entry block
1675                Block enter = (_currentAllocatedSection != null)
1676                        ? _currentAllocatedSection.getEnterBlock(_previousAllocatedSection)
1677                                : null;
1678
1679                if (enter == null || enter.getState() == Block.OCCUPIED) {
1680                    // Start immediately from current position (adjust remaining distance if we’re already partway in)
1681                    float remainingMm = distanceMmBase;
1682                    if (_currentAllocatedSection != null && _currentBlock != null) {
1683                        float sectionLen = _currentAllocatedSection.getActualLength();
1684                        float lenRemaining = _currentAllocatedSection.getLengthRemaining(_currentBlock);
1685                        float progressed = Math.max(0.0f, sectionLen - lenRemaining);
1686                        remainingMm = distanceMmBase - progressed;
1687                    }
1688                    if (remainingMm <= 0.0f) {
1689                        // Already at/inside the target – assert crawl and fall through to sensor wait
1690                        float vMin =
1691                                re.getSpeedProfile().getSpeed(_minReliableOperatingSpeed, _autoEngineer.getIsForward());
1692                        float thrMin = re.getSpeedProfile().getThrottleSetting(vMin, _autoEngineer.getIsForward());
1693
1694                        // Quantize to a real throttle step to avoid values between 0 and the first speed step.
1695                        float q = clampThrottle(thrMin);
1696                        float inc = getThrottle().getSpeedIncrement();
1697                        if (inc > 0.0f) {
1698                            int steps = Math.round(q / inc);
1699                            q = steps * inc;
1700                            if (q > 0.0f && q < inc)
1701                                q = inc;
1702                            q = clampThrottle(q);
1703                        }
1704                        _autoEngineer.setSpeedImmediate(q);
1705                    } else {
1706                        // Cancel first; then mark that we are in a distance-based stop (suppresses setSpeedBySignal correctly)
1707                        cancelStopInCurrentSection();
1708                        _stoppingUsingSpeedProfile = true; // suppress setSpeedBySignal until done
1709                        re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed,
1710                                _maxSpeedScaleKmh, (float) _dispatcher.getScale().getScaleRatio(),
1711                                _autoEngineer.getIsForward());
1712                        // Combined mode: approach to MIN over 'remainingMm', then stop by the section stop sensor.
1713                        // (Use correct API and throttle accessor; do NOT return here.)
1714                        re.getSpeedProfile().planApproachToMinOverDistanceThenStopBySensor(
1715                                getThrottle(), remainingMm, stopSensorCandidate, _speedFactor);
1716
1717                        // Do NOT start the legacy DistanceStopController in combined mode.
1718                    }
1719
1720                    // Now arm the stop sensor, but do NOT pre-lower to a generic stopping speed
1721                    _stopSensor = stopSensorCandidate;
1722                    if (_stopSensor.getKnownState() == Sensor.ACTIVE) {
1723                        setStopNow();  // sensor is already made – stop immediately
1724                    } else {
1725                        _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> {
1726                            handleStopSensorChange(e);
1727                        });
1728                        _stoppingBySensor = true;
1729                    }
1730                    // Ensure stop tasks/termination run when the train actually stops.
1731                    Runnable __waitForStop = new WaitForTrainToStop(task);
1732                    Thread __tWait = jmri.util.ThreadingUtil.newThread(__waitForStop,
1733                            "Wait for stop " + getActiveTrain().getActiveTrainName());
1734                    __tWait.start();
1735
1736                    return; // combined branch handled
1737                }
1738
1739                // Not yet at the section entry: arm a pending approach-to-min plan and the stop sensor listener now
1740                _distanceStopPending = true;
1741                _distanceStopPendingToMin = true;
1742                _distanceStopPendingMm = distanceMmBase;
1743                _distanceStopPendingTask = task;
1744
1745                _stopSensor = stopSensorCandidate;
1746                if (_stopSensor.getKnownState() == Sensor.ACTIVE) {
1747                    setStopNow();
1748                } else {
1749                    _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> {
1750                        handleStopSensorChange(e);
1751                    });
1752                    _stoppingBySensor = true;
1753                }
1754                // Ensure stop tasks/termination run when the train actually stops.
1755                Runnable __waitForStop = new WaitForTrainToStop(_distanceStopPendingTask);
1756                Thread __tWait = jmri.util.ThreadingUtil.newThread(__waitForStop,
1757                        "Wait for stop " + getActiveTrain().getActiveTrainName());
1758                __tWait.start();
1759
1760                return; // wait for entry OCCUPIED to start the approach-to-min plan
1761            }
1762
1763            // --- Legacy pure distance stop (ramp to ZERO at the distance) ---
1764            // Case A/B logic (start now or arm pending), just like before.
1765            Block enter = (_currentAllocatedSection != null)
1766                    ? _currentAllocatedSection.getEnterBlock(_previousAllocatedSection)
1767                            : null;
1768
1769            if (enter == null || enter.getState() == Block.OCCUPIED) {
1770                float remainingMm = distanceMmBase;
1771                if (_currentAllocatedSection != null && _currentBlock != null) {
1772                    float sectionLen = _currentAllocatedSection.getActualLength();
1773                    float lenRemaining = _currentAllocatedSection.getLengthRemaining(_currentBlock);
1774                    float progressed = Math.max(0.0f, sectionLen - lenRemaining);
1775                    remainingMm = distanceMmBase - progressed;
1776                }
1777                if (remainingMm <= 0.0f) {
1778                    setStopNow();
1779                } else {
1780                    _stoppingUsingSpeedProfile = true;
1781                    cancelStopInCurrentSection();
1782
1783                    re.getSpeedProfile().setMinMaxLimitsKmh(_minReliableOperatingSpeed, _maxSpeed, _maxSpeedScaleKmh,
1784                            (float) _dispatcher.getScale().getScaleRatio(), _autoEngineer.getIsForward());
1785                    // Delegate pure distance stop-to-zero to RosterSpeedProfile
1786                    re.getSpeedProfile().planStopToZeroOverDistance(getThrottle(), remainingMm, _speedFactor);
1787                    Thread tWait = jmri.util.ThreadingUtil.newThread(new WaitForTrainToStop(task),
1788                            "Wait for stop " + getActiveTrain().getActiveTrainName());
1789                    tWait.start();
1790                }
1791                return;
1792
1793            }
1794
1795            // Arm pending pure distance stop
1796            _distanceStopPending = true;
1797            _distanceStopPendingToMin = false;
1798            _distanceStopPendingMm = distanceMmBase;
1799            _distanceStopPendingTask = task;
1800            return;
1801        }
1802
1803    }
1804
1805    // =======================================================================
1806        // Do not exit before destination stop logic;
1807        // only bail out if the train is already at zero AND no profile/distance stop is requested.
1808        if (getTargetSpeed() == 0.0f && !_stopBySpeedProfile && _stopByDistanceMm <= 0.0f) {
1809            log.debug("{}: already stopped and no planned stop requested — skipping stop planning.", _activeTrain.getTrainName());
1810            return;
1811        }
1812        // if Section has stopping sensors, use them
1813        if (_currentAllocatedSection.getSection().getState() == Section.FORWARD) {
1814            _stopSensor = _currentAllocatedSection.getSection().getForwardStoppingSensor();
1815        } else {
1816            _stopSensor = _currentAllocatedSection.getSection().getReverseStoppingSensor();
1817        }
1818        // DEBUG
1819        if (_stopSensor != null && !_useStopSensor) {
1820            log.debug("{}: Override enabled - ignoring section stop sensor {}",
1821                    _activeTrain.getTrainName(), _stopSensor.getDisplayName(USERSYS));
1822        }
1823        if (_stopSensor != null && _useStopSensor) {
1824            if (_stopSensor.getKnownState() == Sensor.ACTIVE) {
1825                // stop sensor is already active, stop now
1826                setStopNow();
1827            } else {
1828                setDecreasedSpeedBeforeStop();
1829                _stopSensor.addPropertyChangeListener(_stopSensorListener = (java.beans.PropertyChangeEvent e) -> {
1830                    handleStopSensorChange(e);
1831                });
1832                _stoppingBySensor = true;
1833            }
1834        } else if (useSpeedProfile && _stopBySpeedProfile) {
1835            log.debug("{}: Section [{}] Section Length[{}] Max Train Length [{}] StopBySpeedProfile [{}]. setStopNow", _activeTrain.getTrainName(),
1836                    _currentAllocatedSection.getSection().getDisplayName(USERSYS), _currentAllocatedSection.getActualLength(), getMaxTrainLengthMM(), _stopBySpeedProfile);
1837            // stopping by speed profile uses section length to stop
1838
1839            setTargetSpeedState(STOP_SPEED,useSpeedProfile);
1840
1841        } else if (_currentAllocatedSection.getActualLength()  < getMaxTrainLengthMM()) {
1842            log.debug("{}: Section [{}] Section Length[{}] Max Train Length [{}]. setStopNow({})",
1843                    _activeTrain.getTrainName(),
1844                    _currentAllocatedSection.getSection().getDisplayName(USERSYS),
1845                    _currentAllocatedSection.getActualLength(),
1846                    getMaxTrainLengthMM(), _stopBySpeedProfile);
1847            // train will not fit comfortably in the Section, stop it immediately
1848            setStopNow();
1849        } else if (_activeTrain.getTrainDetection() == TrainDetection.TRAINDETECTION_WHOLETRAIN) {
1850            log.debug("{}: train will fit in [{}] ({}>={}), stop when prev block clears.", _activeTrain.getTrainName(),
1851                    _currentAllocatedSection.getSection().getDisplayName(USERSYS), _currentAllocatedSection.getActualLength(), getMaxTrainLengthMM());
1852            // train will fit in current allocated Section and has resistance wheels
1853            // try to stop by watching Section Block occupancy
1854            if (_currentAllocatedSection.getSection().getNumBlocks() == 1) {
1855                if (_previousAllocatedSection != null) {
1856                    Block tBlock;
1857                    // just because current section has one block does not mean the previous one did.
1858                    if (_previousAllocatedSection.getSection().getNumBlocks() == 1) {
1859                        tBlock = _previousAllocatedSection.getSection().getLastBlock();
1860                    } else {
1861                        tBlock = _previousAllocatedSection.getSection().getExitBlock();
1862                    }
1863                    if ((tBlock != null) && (tBlock.getState() == Block.OCCUPIED)) {
1864                        _stoppingBlock = tBlock;
1865                        setStopByBlockOccupancy(false);
1866                    } else {
1867                        setStopNow();
1868                    }
1869                } else {
1870                    setStopNow();
1871                }
1872            } else {
1873                // Section has multiple blocks
1874                Block exitBlock = _currentAllocatedSection.getExitBlock();
1875                Block enterBlock = _currentAllocatedSection.getEnterBlock(_previousAllocatedSection);
1876                if (enterBlock == null) {
1877                    // this is the first Section of the Transit, with train starting in this Section
1878                    setStopNow();
1879                } else if (exitBlock == enterBlock) {
1880                    // entry and exit are from the same Block
1881                    if ((_previousBlock != null) && (_previousBlock.getState() == Block.OCCUPIED)
1882                            && (getBlockLength(exitBlock) > getMaxTrainLengthMM())) {
1883                        _stoppingBlock = _previousBlock;
1884                        setStopByBlockOccupancy(false);
1885                    } else {
1886                        setStopNow();
1887                    }
1888                } else {
1889                    // try to move train as far into the Section as it will comfortably fit
1890                    Block tstBlock = exitBlock;
1891                    if (tstBlock == null) {
1892                        if (_currentAllocatedSection.getDirection() == Section.REVERSE) {
1893                            tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber(0);
1894                        } else {
1895                            tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber(
1896                                    _currentAllocatedSection.getSection().getNumBlocks() - 1);
1897                        }
1898                    }
1899                    int tstLength = getBlockLength(tstBlock);
1900                    int tstBlockSeq = _currentAllocatedSection.getSection().getBlockSequenceNumber(tstBlock);
1901                    while ((tstLength < getMaxTrainLengthMM()) && (tstBlock != enterBlock)) {
1902                        int newSeqNumber;
1903                        if (_currentAllocatedSection.getDirection() == Section.REVERSE) {
1904                            newSeqNumber = tstBlockSeq + 1;
1905                        } else {
1906                            newSeqNumber = tstBlockSeq - 1;
1907                        }
1908                        tstBlock = _currentAllocatedSection.getSection().getBlockBySequenceNumber(newSeqNumber);
1909                        tstBlockSeq = newSeqNumber;
1910                        tstLength += getBlockLength(tstBlock);
1911                    }
1912                    if (getMaxTrainLengthMM() > tstLength) {
1913                        setStopNow();
1914                    } else if (tstBlock == enterBlock) {
1915                        // train fits, but needs all available Blocks
1916                        Block previousSectionExitBlock = _previousAllocatedSection.getExitBlock();
1917                        if ((previousSectionExitBlock != null) && (previousSectionExitBlock.getState() == Block.OCCUPIED)) {
1918                            _stoppingBlock = previousSectionExitBlock;
1919                            setStopByBlockOccupancy(true);
1920                        } else {
1921                            setStopNow();
1922                        }
1923                    } else {
1924                        // train fits, and doesn't need all available Blocks
1925                        int xSeqNumber = tstBlockSeq + 1;
1926                        if (_currentAllocatedSection.getDirection() == Section.FORWARD ) {
1927                            xSeqNumber = tstBlockSeq - 1;
1928                        }
1929                        _stoppingBlock = _currentAllocatedSection.getSection().
1930                                getBlockBySequenceNumber(xSeqNumber);
1931                        setStopByBlockOccupancy(true);
1932                    }
1933                }
1934            }
1935        } else {
1936            // train will fit, but no way to stop it reliably
1937            setStopNow();
1938        }
1939
1940        // even if no task is required it must be run
1941        // as cleanup happens after train stops.
1942        Runnable waitForStop = new WaitForTrainToStop(task);
1943        Thread tWait = jmri.util.ThreadingUtil.newThread(waitForStop, "Wait for stop " + getActiveTrain().getActiveTrainName());
1944        tWait.start();
1945    }
1946
1947    protected synchronized void executeStopTasks(int task) {
1948        // clean up stopping
1949        cancelStopInCurrentSection();
1950        _stoppingUsingSpeedProfile = false;  // queued stop has completed; allow normal speed logic again
1951        _dispatcher.queueReleaseOfCompletedAllocations();
1952        log.trace("exec[{}]",task);
1953        switch (task) {
1954            case END_TRAIN:
1955                _activeTrain.setStatus(ActiveTrain.DONE);
1956                break;
1957            case NO_TASK:
1958                // clean up stop
1959                break;
1960            case END_REVERSAL:
1961                /* Reset _previousBlock to be the _currentBlock if we do a continious reverse otherwise the stop in block method fails
1962                to stop the loco in the correct block
1963                 if the first block we come to has a stopped or held signal */
1964                _activeTrain.setRestart(_activeTrain.getDelayReverseRestart(),_activeTrain.getReverseRestartDelay(),
1965                        _activeTrain.getReverseRestartSensor(),_activeTrain.getResetReverseRestartSensor());
1966                _activeTrain.setTransitReversed(true);
1967                _activeTrain.reverseAllAllocatedSections();
1968                setEngineDirection();
1969                _previousBlock = null;
1970                _nextBlock = getNextBlock(_currentBlock,_currentAllocatedSection);
1971                if (_activeTrain.getDelayReverseRestart() == ActiveTrain.NODELAY) {
1972                    _activeTrain.holdAllocation(false);
1973                    // a reversal can happen in mid section
1974                    setupNewCurrentSignal(_currentAllocatedSection, true);
1975                    setSpeedBySignal();
1976                    if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) {
1977                        _dispatcher.queueScanOfAllocationRequests();
1978                        break;
1979                    }
1980                }
1981                break;
1982            case BEGINNING_RESET:
1983                _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(),
1984                        _activeTrain.getRestartSensor(),_activeTrain.getResetRestartSensor());
1985                if (_activeTrain.getResetWhenDone()) {
1986                    if (_activeTrain.getDelayedRestart() == ActiveTrain.NODELAY && !_activeTrain.getReverseAtEnd()) {
1987                        log.error("[{}]: train is continueing without pause, should have been handled in handleBlockStateChange.",_activeTrain.getTrainName());
1988                    } else {
1989                        // then active train is delayed
1990                        _activeTrain.setTransitReversed(false);
1991                        _activeTrain.resetAllAllocatedSections();
1992                        _previousBlock = null;
1993                        _nextBlock = getNextBlock(_currentBlock,_currentAllocatedSection);
1994                        setEngineDirection();
1995                        _activeTrain.setRestart(_activeTrain.getDelayedRestart(),_activeTrain.getRestartDelay(),
1996                                _activeTrain.getRestartSensor(), _activeTrain.getResetRestartSensor());
1997                        if ((_nextSection != null) && !_activeTrain.isInAllocatedList(_nextSection)) {
1998                            _dispatcher.queueScanOfAllocationRequests();
1999                        }
2000                        // can be mid block
2001                        setupNewCurrentSignal(null, true);
2002                        setSpeedBySignal();
2003
2004                    }
2005                } else {
2006                    // dispatcher cancelled auto restart while train was stopping?
2007                    log.warn("[{}]resetWhenDone flag reset, likely user cancelling while processing stop",
2008                            _activeTrain.getActiveTrainName());
2009                }
2010                break;
2011            default:
2012                log.debug("[{}]Invalid action [{}] in executeStopTasksRequest to execute BEGINNING_RESET cancelled", _activeTrain.getActiveTrainName(),task);
2013                break;
2014        }
2015    }
2016
2017    /**
2018     * Remove the stopping sensor
2019     */
2020    private void cancelStoppingBySensor() {
2021        if (_stopSensor != null) {
2022            _stopSensor.removePropertyChangeListener(_stopSensorListener);
2023            _stoppingBySensor = false;
2024            _stopSensorListener = null;
2025            _stopSensor = null;
2026        }
2027    }
2028
2029    /**
2030     * When the stopping sensor we are waiting on goes active
2031     * stop the train or set a new speed and destroy itself
2032     * @param e  - the property change event
2033     */
2034    private synchronized void handleStopSensorChange(java.beans.PropertyChangeEvent e) {
2035        if (e.getPropertyName().equals("KnownState") && (int) e.getNewValue() == Sensor.ACTIVE) {
2036            _stopSensor.removePropertyChangeListener(_stopSensorListener);
2037            _stoppingBySensor = false;
2038            _stopSensorListener = null;
2039            _stopSensor = null;
2040            if (_needSetSpeed) {
2041                _needSetSpeed = false;
2042                setSpeedBySignal();
2043            } else {
2044                setStopNow();
2045            }
2046        }
2047    }
2048
2049    private synchronized void setStopNow() {
2050        setStopNow(false);
2051    }
2052
2053    private synchronized void setStopNow(boolean useSpeedProfile) {
2054        setTargetSpeedState(STOP_SPEED,useSpeedProfile);
2055        if (_currentAllocatedSection == null) {  // this may occur if the train is not in the selected block when initially created and the signal is held.
2056            _activeTrain.setStatus(ActiveTrain.WAITING);
2057        } else if (_currentAllocatedSection.getNextSection() == null) {
2058            // wait for train to stop - this lets action items complete in a timely fashion
2059            waitUntilStopped();
2060            _activeTrain.setStatus(ActiveTrain.DONE);
2061        } else {
2062            _activeTrain.setStatus(ActiveTrain.WAITING);
2063        }
2064    }
2065
2066    /*
2067     * When multi block stopping, the stopping block may not be occupied yet.
2068     */
2069    private void setStopByBlockOccupancy(boolean ignoreNotOccupied) {
2070        // note: _stoppingBlock must be set before invoking this method
2071        //  verify that _stoppingBlock is actually occupied, if not stop immediately
2072        if (_stoppingBlock.getState() == Block.OCCUPIED || ignoreNotOccupied) {
2073            setDecreasedSpeedBeforeStop();
2074            _stoppingByBlockOccupancy = true;
2075        } else {
2076            setStopNow();
2077        }
2078    }
2079
2080    /**
2081     * Before stopping by sensor alone, or by clearing previous block,
2082     * set the speed to the user defined preference.
2083     */
2084    private void setDecreasedSpeedBeforeStop() {
2085        float signalSpeed = 25;
2086        try {
2087            signalSpeed = InstanceManager.getDefault(SignalSpeedMap.class)
2088                    .getSpeed(_dispatcher.getStoppingSpeedName());
2089        } catch (IllegalArgumentException ex) {
2090            log.error("Missing [{}] from Speed table - defaulting to 25",
2091                    _dispatcher.getStoppingSpeedName());
2092        }
2093        if (getThrottleSettingFromSpeed(signalSpeed) < getTargetSpeed()) {
2094            if (useSpeedProfile) {
2095                // use 75 percent or normal amount, dont clear isstopping for ramping.
2096                setTargetSpeedByProfile(signalSpeed,_stopBySpeedProfileAdjust*0.75f,false);
2097            } else {
2098                setTargetSpeed(signalSpeed/100.0f);
2099            }
2100        }
2101    }
2102
2103    ///**
2104    // * Sets the throttle percent unless it is already less than the new setting
2105    // * @param throttleSetting  Max ThrottleSetting required.
2106    // */
2107    //private synchronized void setToAMaximumThrottle(float throttleSetting) {
2108    //    if (throttleSetting < getTargetSpeed()) {
2109    //        setTargetSpeed(throttleSetting);
2110    //    }
2111    //}
2112
2113    /**
2114     * Calculates the throttle setting for a given speed.
2115     * @param speed  the unadjusted speed.
2116     * @return - throttle setting (a percentage)
2117     */
2118    private synchronized float getThrottleSettingFromSpeed(float speed) {
2119        if (useSpeedProfile) {
2120            float throttleSetting = _activeTrain.getRosterEntry().getSpeedProfile()
2121                    .getThrottleSettingFromSignalMapSpeed(speed, getForward());
2122            return throttleSetting;
2123        }
2124        if (_activeTrain.getSignalType() == DispatcherFrame.SIGNALMAST) {
2125            float mls;
2126            if (_controllingSignalMast != null) {
2127                mls = _controllingSignalMast.getSignalSystem().getMaximumLineSpeed();
2128            } else {
2129                //plan B
2130                mls = _dispatcher.getMaximumLineSpeed();
2131            }
2132            float throttleSetting = (speed / mls);
2133            return throttleSetting;
2134        } else
2135            return speed/100.0f;
2136    }
2137
2138
2139    /**
2140     * sets the throttle based on an index number into _speedRatio array
2141     * @param speedState  Index value
2142     */
2143    private synchronized void setTargetSpeedState(int speedState) {
2144        setTargetSpeedState(speedState,false);
2145    }
2146
2147    /**
2148     * sets the throttle based on an index number into _speedRatio array
2149     * @param speedState  Index value
2150     * @param stopBySpeedProfile if true use speed profile
2151     */
2152    private synchronized void setTargetSpeedState(int speedState,boolean stopBySpeedProfile) {
2153        log.trace("{}: setTargetSpeedState:({})",_activeTrain.getTrainName(),speedState);
2154        if (_currentAllocatedSection == null) {
2155            log.debug("_currentAllocatedSection == null in setTargetSpeedState");
2156            return;
2157        }
2158        _autoEngineer.slowToStop(false);
2159
2160        float stoppingDistanceAdjust =  _stopBySpeedProfileAdjust *
2161                ( _activeTrain.isTransitReversed() ?
2162                        _currentAllocatedSection.getTransitSection().getRevStopPerCent() :
2163                            _currentAllocatedSection.getTransitSection().getFwdStopPerCent());
2164        log.debug("stoppingDistanceAdjust[{}] isReversed[{}] stopBySpeedProfileAdjust[{}]",stoppingDistanceAdjust,
2165                _activeTrain.isTransitReversed(),_stopBySpeedProfileAdjust );
2166        if (speedState > STOP_SPEED) {
2167            cancelStopInCurrentSection();
2168            if (_currentRampRate == RAMP_SPEEDPROFILE && useSpeedProfile) {
2169                // we are going to ramp up  / down using section length and speed profile
2170                _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock)
2171                        * stoppingDistanceAdjust, speedState);
2172            } else {
2173                setTargetSpeed(_speedRatio[speedState]);
2174            }
2175        } else if (stopBySpeedProfile) {
2176            // we are going to stop by profile
2177            _stoppingUsingSpeedProfile = true;
2178            _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock)
2179                    * stoppingDistanceAdjust, 0.0f);
2180        } else {
2181            _autoEngineer.setHalt(true);
2182            setTargetSpeed(0.0f);
2183        }
2184    }
2185
2186    private synchronized void setTargetSpeedByProfile(float speedState, float stopBySpeedProfileAdjust, boolean cancelStopping) {
2187        // the speed comes in as units of warrents (mph, kph, mm/s etc)
2188        try {
2189            float throttleSetting = _activeTrain.getRosterEntry().getSpeedProfile().getThrottleSettingFromSignalMapSpeed(speedState, getForward());
2190            log.debug("{}: setTargetSpeedByProfile: {} SpeedState[{}]",
2191                    _activeTrain.getTrainName(),
2192                    throttleSetting,
2193                    speedState);
2194            if (throttleSetting > 0.009 && _currentRampRate != RAMP_SPEEDPROFILE && useSpeedProfile) {
2195                if (cancelStopping) {cancelStopInCurrentSection();}
2196                setTargetSpeed(throttleSetting); // apply speed factor and max
2197            } else if (throttleSetting > 0.009) {
2198                if (cancelStopping) {cancelStopInCurrentSection();}
2199                setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock)  * stopBySpeedProfileAdjust , throttleSetting);
2200            } else if (useSpeedProfile && _stopBySpeedProfile) {
2201                setTargetSpeed(0.0f);
2202                _stoppingUsingSpeedProfile = true;
2203                _autoEngineer.setTargetSpeed(_currentAllocatedSection.getLengthRemaining(_currentBlock)  * stopBySpeedProfileAdjust, 0.0f);
2204            } else {
2205                _autoEngineer.slowToStop(false);
2206                setTargetSpeed(0.0f);
2207                _autoEngineer.setHalt(true);
2208            }
2209        } catch (Exception ex) {
2210            log.error("setTargetSpeedByProfile crashed - Emergency Stop: ", ex );
2211            _autoEngineer.slowToStop(false);
2212            setTargetSpeed(-1.0f);
2213            _autoEngineer.setHalt(true);
2214        }
2215    }
2216
2217    /**
2218     * Pass in speed as shown on dialogs, and convert to decimal speed needed by
2219     * throttle.
2220     */
2221    private synchronized void setTargetSpeedValue(float speed) {
2222        log.debug("{}: setTargetSpeedValue: Speed[{}]",_activeTrain.getTrainName(),speed);
2223        if (useSpeedProfile) {
2224            setTargetSpeedByProfile(speed,_stopBySpeedProfileAdjust,true);
2225            return;
2226        }
2227        _autoEngineer.slowToStop(false);
2228        float mls;
2229        if (_controllingSignalMast != null) {
2230            mls = _controllingSignalMast.getSignalSystem().getMaximumLineSpeed();
2231        } else {
2232            mls = _dispatcher.getMaximumLineSpeed();
2233        }
2234        float decSpeed = (speed / mls);
2235        if (decSpeed > 0.0f) {
2236            cancelStopInCurrentSection();
2237            setTargetSpeed(decSpeed);
2238        } else {
2239            setTargetSpeed(0.0f);
2240            _autoEngineer.setHalt(true);
2241        }
2242    }
2243
2244    private int getBlockLength(Block b) {
2245        if (b == null)
2246            return (0);
2247        return (int) b.getLengthMm();
2248        //        float fLength = b.getLengthMm() / (float) _dispatcher.getScale().getScaleFactor();
2249        //        if (_dispatcher.getUseScaleMeters()) {
2250        //            return (int) (fLength * 0.001f);
2251        //        }
2252        //        return (int) (fLength * 0.00328084f);
2253    }
2254
2255    /**
2256     * Initiates running in manual mode with external throttle.
2257     * <p>
2258     * This method is triggered by an action in the Transit. The throttle in use
2259     * for automatic operation is dispatched.
2260     */
2261    protected void initiateWorking() {
2262        if (_activeTrain.getStatus() != ActiveTrain.WORKING) {
2263            _activeTrain.setMode(ActiveTrain.DISPATCHED);
2264            _activeTrain.setStatus(ActiveTrain.WORKING);
2265            saveSpeedAndDirection();
2266            if (_autoEngineer != null) {
2267                _autoEngineer.setHalt(true);
2268                waitUntilStopped();
2269                _autoEngineer.abort();
2270                InstanceManager.throttleManagerInstance().releaseThrottle(_throttle, this);
2271                _autoEngineer = null;
2272                _throttle = null;
2273            }
2274        }
2275    }
2276
2277    /**
2278     * Returns when train is stopped.
2279     * <p>
2280     * Note: Provides for _autoEngineer becoming null during wait Ties up the
2281     * current autoActiveTrain thread.
2282     */
2283    protected void waitUntilStopped() {
2284        boolean doneWaiting = false;
2285        while (!doneWaiting) {
2286            if (_autoEngineer != null) {
2287                doneWaiting = _autoEngineer.isStopped();
2288            } else {
2289                doneWaiting = true;
2290            }
2291            if (!doneWaiting) {
2292                try {
2293                    Thread.sleep(50);
2294                } catch (InterruptedException e) {
2295                    // ignore this exception
2296                }
2297            }
2298        }
2299    }
2300
2301    /**
2302     * Resumes automatic running after a working session using an external
2303     * throttle This method is triggered by the dispatcher hitting the "Resume
2304     * Auto Running" button A new throttle is acquired to allow automatic
2305     * running to resume
2306     */
2307    protected void resumeAutomaticRunning() {
2308        if ((_activeTrain.getStatus() == ActiveTrain.WORKING)
2309                || (_activeTrain.getStatus() == ActiveTrain.READY)) {
2310            _autoTrainAction.cancelDoneSensor();
2311            if (initialize()) {
2312                _resumingAutomatic = true;
2313            } else {
2314                log.error("Failed to initialize throttle when resuming automatic mode.");
2315            }
2316        }
2317    }
2318
2319    /**
2320     * Pause the auto active train for a specified number of fast clock minutes.
2321     *
2322     * @param fastMinutes the number of minutes to pause the train
2323     * @return the thread waiting on the pause or null if already paused
2324     */
2325    public Thread pauseTrain(int fastMinutes) {
2326        if (_pausingActive)
2327            // if a pause train thread is currently active, ignore this call
2328            return (null);
2329        Runnable pauseTrain = new PauseTrain(fastMinutes);
2330        Thread tPause = jmri.util.ThreadingUtil.newThread(pauseTrain, "pause train " + _activeTrain.getTrainName());
2331        tPause.start();
2332        return tPause;
2333    }
2334
2335    public void terminate() {
2336        // here add code to stop the train and release its throttle if it is in autoRun
2337        while (_activeHornThreads > 0) {
2338            try {
2339                Thread.sleep(50);
2340            } catch (InterruptedException e) {
2341                // ignore this exception
2342            }
2343        }
2344        _autoTrainAction.clearRemainingActions();
2345        if (_autoEngineer != null) {
2346            _autoEngineer.setHalt(true);
2347            try {
2348                Thread.sleep(50);
2349            } catch (InterruptedException e) {
2350                // ignore this exception
2351            }
2352            waitUntilStopped();
2353            _autoEngineer.abort();
2354            InstanceManager.throttleManagerInstance().releaseThrottle(_throttle, this);
2355        }
2356    }
2357
2358    public void dispose() {
2359        if (_controllingSignalMast != null && _conSignalMastListener != null) {
2360            _controllingSignalMast.removePropertyChangeListener(_conSignalMastListener);
2361        }
2362        _controllingSignalMast = null;
2363        _conSignalMastListener = null;
2364        if (_turnoutStateNeeded != null && _turnoutStateListener != null) {
2365            _turnoutStateNeeded.removePropertyChangeListener(_turnoutStateListener);
2366        }
2367        _turnoutStateNeeded = null;
2368        _turnoutStateListener = null;
2369    }
2370
2371    // _________________________________________________________________________________________
2372    // This class waits for train stop in a separate thread
2373    class WaitForTrainToStop implements Runnable {
2374
2375        public WaitForTrainToStop(int task) {
2376            _task = task;
2377        }
2378
2379        @Override
2380        public void run() {
2381            boolean waitingOnTrain = true;
2382            try {
2383                while (waitingOnTrain) {
2384                    if ((getAutoEngineer() != null) && (getAutoEngineer().isStopped())) {
2385                        waitingOnTrain = false;
2386                    } else {
2387                        Thread.sleep(_delay);
2388                    }
2389                }
2390                log.trace("executing task[{}]",_task);
2391                executeStopTasks(_task);
2392            } catch (InterruptedException e) {
2393                log.warn("Waiting for train to stop interrupted - stop tasks not executing");
2394            } catch (Exception e) {
2395                log.error("Waiting for train to stop crashed - stop tasks not executing.", e);
2396            }
2397        }
2398
2399        private final int _delay = 91;
2400        private int _task = 0;
2401    }
2402
2403    /**
2404     * Pause the train in a separate thread. Train is stopped, then restarted
2405     * after specified number of fast Minutes have elapsed.
2406     */
2407    class PauseTrain implements Runnable {
2408        /**
2409         * Create a PauseTrain
2410         *
2411         * @param fastMinutes the number of fast clock minutes to pause the
2412         *                    train
2413         */
2414        public PauseTrain(int fastMinutes) {
2415            _fastMinutes = fastMinutes;
2416        }
2417
2418        @Override
2419        public void run() {
2420            // set to pause at a fast ramp rate
2421            _pausingActive = true;
2422            // TODO: use stop in section or block?
2423            _savedRampRate = getRampRate();
2424            setCurrentRampRate(RAMP_FAST);
2425            stopInCurrentSection(NO_TASK);
2426            // wait for train to stop
2427            boolean waitNow = true;
2428            boolean keepGoing = true;
2429            while (waitNow) {
2430                try {
2431                    Thread.sleep(101);
2432                    if (_autoEngineer != null) {
2433                        if (_autoEngineer.isStopped()) {
2434                            waitNow = false;
2435                        }
2436                    } else {
2437                        waitNow = false;
2438                    }
2439                } catch (InterruptedException e) {
2440                    log.trace("InterruptedException while waiting to stop for pause-indicates action cancelled.", e);
2441                    waitNow = false;
2442                    keepGoing = false;
2443                }
2444            }
2445            _activeTrain.setStatus(ActiveTrain.PAUSED);
2446            if (keepGoing) {
2447                // wait for specified fast clock time
2448                Timebase _clock = InstanceManager.getDefault(jmri.Timebase.class);
2449                java.beans.PropertyChangeListener _clockListener = (java.beans.PropertyChangeEvent e) -> {
2450                    _fastMinutes--;
2451                };
2452                _clock.addMinuteChangeListener(_clockListener);
2453                // wait for fast minutes to tick away
2454                waitNow = true;
2455                while (waitNow) {
2456                    try {
2457                        Thread.sleep(501);
2458                        if (_fastMinutes <= 0) {
2459                            waitNow = false;
2460                        }
2461                    } catch (InterruptedException e) {
2462                        log.trace("InterruptedException indicates action cancelled.", e);
2463                        keepGoing = false;
2464                    }
2465                }
2466                _clock.removeMinuteChangeListener(_clockListener);
2467            }
2468            _pausingActive = false;
2469            if (keepGoing) {
2470                // this thread was not interrupted
2471                //   resume running - restore speed, status, and ramp rate
2472                setCurrentRampRate(_savedRampRate);
2473                // Set speed by signal also works if signal missing
2474                // so we dont need to restore a previous value.
2475                _activeTrain.setStatus(ActiveTrain.RUNNING);
2476                setSpeedBySignal();
2477            }
2478        }
2479        private int _fastMinutes = 0;
2480        private int _savedRampRate = RAMP_NONE;
2481    }
2482
2483    // _________________________________________________________________________________________
2484    // this class handles the interface with the throttle
2485    // (This class started from code by Pete Cressman contained in Warrant.java.)
2486    class AutoEngineer  {
2487
2488        AutoEngineer(DccThrottle throttle, RosterEntry rosterEntry) {
2489            this.throttle = throttle;
2490            this.rosterEntry = rosterEntry;
2491        }
2492
2493        private DccThrottle throttle;
2494        private int ramping;
2495        private boolean speedProfileStoppingIsRunning = false;
2496        private float speedIncrement = 0.0f; //will be recalculated
2497        private float targetSpeed;
2498        private RosterEntry rosterEntry;
2499        private int throttleInterval;
2500        private float minReliableOperatingSpeed;
2501        private float maxSpeed;
2502        private float speedFactor;
2503
2504        public void setRamping(int ramping, int fullRampTime, int minThrottleInterval, int rampRate) {
2505            this.ramping = ramping;
2506            this.throttleInterval = minThrottleInterval;
2507            //calculate speed increment to use in each minInterval time
2508            speedIncrement = (100.0f / ((float) fullRampTime / minThrottleInterval)
2509                    / rampRate) / 100.0f;
2510            log.debug("{}: _speedIncrement={}", throttle.getLocoAddress(), speedIncrement);
2511        }
2512
2513        // Once physics ramping is found to be unusable for this train, permanently disable it
2514        // for the remainder of this AutoEngineer instance to avoid repeated stalls or repeated warnings.
2515        private boolean physicsRampingDisabled = false;
2516
2517        private void disablePhysicsRamping(String reason, float weightKg, float powerKw, float tractiveEffortKn) {
2518            if (!physicsRampingDisabled) {
2519                String id = (rosterEntry != null) ? rosterEntry.getId() : "<unknown>";
2520                log.warn(
2521                        "{}: Physics ramp disabled ({}). Roster physics: weightKg={}, powerKw={}, tractiveEffortKn={}; forcing RAMP_MEDIUM.",
2522                        id, reason, Float.valueOf(weightKg), Float.valueOf(powerKw), Float.valueOf(tractiveEffortKn));
2523            }
2524            physicsRampingDisabled = true;
2525
2526            // Ensure the AutoActiveTrain state is no longer RAMP_PHYSICS
2527            AutoActiveTrain.this.setRampRate(RAMP_MEDIUM);
2528
2529            // Ensure this AutoEngineer instance is no longer in physics mode
2530            this.ramping = RAMP_MEDIUM;
2531
2532            // Recompute ramp parameters for medium ramp so speedIncrement matches the selected mode
2533            if (AutoActiveTrain.this._dispatcher != null) {
2534                setRamping(RAMP_MEDIUM, AutoActiveTrain.this._dispatcher.getFullRampTime(),
2535                        AutoActiveTrain.this._dispatcher.getMinThrottleInterval(), RAMP_MEDIUM);
2536            }
2537        }
2538
2539
2540        public  void setIsForward(boolean isForward) {
2541            throttle.setIsForward(isForward);
2542        }
2543
2544        public boolean getIsForward() {
2545            return(throttle.getIsForward());
2546        }
2547
2548        public void setTargetSpeed(float speed) {
2549            stopAllTimers();
2550
2551            // Physics ramp: only if enabled AND speed profile exists for current direction
2552            boolean physicsRamp = (ramping == RAMP_PHYSICS);
2553            boolean forward = getIsForward();
2554            boolean profileAvailable = false;
2555            if (AutoActiveTrain.this.re != null && AutoActiveTrain.this.re.getSpeedProfile() != null) {
2556                profileAvailable = forward
2557                        ? AutoActiveTrain.this.re.getSpeedProfile().hasForwardSpeeds()
2558                                : AutoActiveTrain.this.re.getSpeedProfile().hasReverseSpeeds();
2559            }
2560
2561
2562            log.debug("[{}] setTargetSpeed: ramping={}, physicsRamp={}, profileAvailable={}, forward={}, speedArg={}",
2563                    AutoActiveTrain.this._activeTrain.getTrainName(),
2564                    ramping, physicsRamp, profileAvailable, forward, speed);
2565
2566
2567
2568            // If physics ramping is selected, ensure a usable speed profile and defined physics parameters exist.
2569            // If not, permanently fall back to RAMP_MEDIUM for this Auto Active Train.
2570            if (physicsRamp) {
2571                if (physicsRampingDisabled) {
2572                    physicsRamp = false;
2573                } else if (!profileAvailable) {
2574                    disablePhysicsRamping("no speed profile for current direction", 0.0f, 0.0f, 0.0f);
2575                    physicsRamp = false;
2576                } else {
2577                    float wKg = 0.0f;
2578                    float pKw = 0.0f;
2579                    float teKn = 0.0f;
2580                    try {
2581                        if (rosterEntry != null) {
2582                            wKg = rosterEntry.getPhysicsWeightKg();
2583                            pKw = rosterEntry.getPhysicsPowerKw();
2584                            teKn = rosterEntry.getPhysicsTractiveEffortKn();
2585                        }
2586                    } catch (Throwable ex) {
2587                        // Older roster entries may not have physics fields
2588                        wKg = 0.0f;
2589                        pKw = 0.0f;
2590                        teKn = 0.0f;
2591                    }
2592                    if ((wKg <= 0.0f) && (pKw <= 0.0f) && (teKn <= 0.0f)) {
2593                        disablePhysicsRamping("no physics parameters defined", wKg, pKw, teKn);
2594                        physicsRamp = false;
2595                    }
2596                }
2597            }
2598            if (physicsRamp && profileAvailable) {
2599                // Physics ramp drives throttle asynchronously via RosterSpeedProfile; keep targetSpeed in sync
2600                // so higher-level stop logic does not treat a moving train as already stopped.
2601                targetSpeed = applyMaxThrottleAndFactor(speed);
2602                // Mark that a RosterSpeedProfile timer/queue may be active so stopAllTimers() can cancel it on terminate.
2603                speedProfileStoppingIsRunning = true;
2604
2605                // Run physics planner off the EDT
2606                Thread phys = jmri.util.ThreadingUtil.newThread(() -> {
2607                    // Ensure min/max limits (including optional scale km/h cap) are in the profile
2608                    re.getSpeedProfile().setMinMaxLimitsKmh(
2609                        minReliableOperatingSpeed,
2610                        maxSpeed,
2611                        AutoActiveTrain.this._maxSpeedScaleKmh,
2612                        (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(),
2613                        forward
2614                    );
2615                    // Delegate the acceleration plan & execution to RosterSpeedProfile
2616                    re.getSpeedProfile().runPhysicsAccelerationToTargetThrottle(
2617                        throttle,
2618                        speed,
2619                        AutoActiveTrain.this._driverPowerPercent,
2620                        AutoActiveTrain.this._additionalWeightTonnes,
2621                        AutoActiveTrain.this._rollingResistanceCoeff,
2622                        (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(),
2623                        speedFactor
2624                    );
2625                }, "PhysicsRamp " + AutoActiveTrain.this._activeTrain.getTrainName());
2626                           phys.start();
2627                return;
2628            }
2629            // Fallback to existing behaviour
2630            targetSpeed = applyMaxThrottleAndFactor(speed);
2631            log.debug("setTargetSpeed: Set Speed[{}] adjusted to TargetSpeed[{}] ", speed, targetSpeed);
2632            if (ramping == RAMP_NONE || ramping == RAMP_SPEEDPROFILE) {
2633                throttle.setSpeedSetting(targetSpeed);
2634            } else {
2635                rampToTarget();
2636            }
2637        }
2638
2639        public float getTargetSpeed(){
2640            return(targetSpeed);
2641        }
2642
2643        /**
2644         *
2645         * @param throttleSetting the throttle setting that would normally be set
2646         * @return the adjusted throttle setting after applying Max Throttle and Percentage throttle settings
2647         */
2648        private float applyMaxThrottleAndFactor(float throttleSetting) {
2649            // Apply speedFactor first (this is how the existing code behaves)
2650            float applied = (throttleSetting > 0.0f) ? (throttleSetting * speedFactor) : throttleSetting;
2651
2652            if (applied <= 0.0f)
2653                return applied;
2654
2655            // Compute the active upper cap:
2656            //  - If a scale km/h cap is set AND a speed profile exists in the current direction,
2657            //    derive an equivalent throttle cap using the roster profile + layout scale ratio.
2658            //  - Otherwise, fall back to the throttle % cap (maxSpeed).
2659            float maxApplied;
2660            boolean forward = getIsForward();
2661            boolean profileAvailable = false;
2662            if (AutoActiveTrain.this.re != null && AutoActiveTrain.this.re.getSpeedProfile() != null) {
2663                // Direction-aware availability
2664                profileAvailable = forward ? AutoActiveTrain.this.re.getSpeedProfile().hasForwardSpeeds()
2665                        : AutoActiveTrain.this.re.getSpeedProfile().hasReverseSpeeds();
2666            }
2667
2668            if (AutoActiveTrain.this._maxSpeedScaleKmh > 0.0f && profileAvailable && AutoActiveTrain.this._dispatcher != null) {
2669                // scale km/h -> actual mm/s
2670                float kmh = AutoActiveTrain.this._maxSpeedScaleKmh;
2671                float scaleRatio = (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio();
2672                float modelKmh = kmh / ((scaleRatio <= 0.0f) ? 1.0f : scaleRatio);
2673                float targetMms = modelKmh * 277.7778f; // 1 km/h = 277.7778 mm/s
2674                // Invert the roster profile to get the required throttle [% 0..1] via RosterSpeedProfile
2675                float thrCapPct = AutoActiveTrain.this.re.getSpeedProfile().getThrottleSetting(targetMms, forward);
2676                // This cap applies to the FINAL applied throttle (after speedFactor),
2677                // so clamp 'applied' directly to thrCapPct.
2678                maxApplied = thrCapPct;
2679            } else {
2680                // Fallback to the existing throttle % cap
2681                maxApplied = maxSpeed;
2682            }
2683
2684            // Enforce min and max caps
2685            if (applied > maxApplied) { applied = maxApplied; }
2686            if (applied < minReliableOperatingSpeed) { applied = minReliableOperatingSpeed; }
2687
2688            return applied;
2689        }
2690
2691        /**
2692         * Flag from user's control.
2693         *
2694         * @param halt true to immediately stop the train; false otherwise
2695         */
2696        public void setHalt(boolean halt) {
2697            if (halt) {
2698                this.setSpeedImmediate(0.0f);
2699            }
2700        }
2701
2702        /**
2703         * Set the limits and adjustment factore for train speed.
2704         * Active train will calculate the required setting and it will be adjusted if not 0.0f
2705         * required setting * speed Factor  then test for less than max and greater than min.
2706         * @param minReliableOperatingSpeed lowest throttle % train will reliably move.
2707         * @param maxSpeed max throttle % for train.
2708         * @param speedFactor multiplier
2709         */
2710        public void setSpeedLimits(float minReliableOperatingSpeed, float maxSpeed, float speedFactor) {
2711            this.minReliableOperatingSpeed = minReliableOperatingSpeed;
2712            this.maxSpeed = maxSpeed;
2713            this.speedFactor = speedFactor;
2714        }
2715
2716        public void setTargetSpeed(float distance, float speed) {
2717            log.debug("Set Target Speed[{}] with distance{{}] from speed[{}]",speed,distance,throttle.getSpeedSetting());
2718            stopAllTimers();
2719            if (rosterEntry != null) {
2720                rosterEntry.getSpeedProfile().setExtraInitialDelay(1500f);
2721                rosterEntry.getSpeedProfile().setMinMaxLimitsKmh(minReliableOperatingSpeed, maxSpeed,
2722                        AutoActiveTrain.this._maxSpeedScaleKmh,
2723                        (float) AutoActiveTrain.this._dispatcher.getScale().getScaleRatio(), getIsForward());
2724                rosterEntry.getSpeedProfile().changeLocoSpeed(_throttle, distance, speed);
2725                speedProfileStoppingIsRunning = true;
2726                targetSpeed = speed;
2727            } else {
2728                setTargetSpeed((0.0f));
2729            }
2730        }
2731
2732        public void slowToStop(boolean on) {
2733            stopAllTimers();
2734            if (on) {
2735                log.debug("SlowToStopOn");
2736                setTargetSpeed((0.0f));
2737            }
2738        }
2739
2740        public void stopAllTimers() {
2741            if (speedProfileStoppingIsRunning) {
2742                re.getSpeedProfile().cancelSpeedChange();
2743                speedProfileStoppingIsRunning = false;
2744            }
2745            if (rampingTimer != null) {
2746                rampingTimer.stop();
2747                rampingTimer = null;
2748            }
2749        }
2750
2751        LinkedList<SpeedSetting> stepQueue;
2752        private javax.swing.Timer rampingTimer;
2753
2754        private void rampToTarget() {
2755            // target already adjusted.
2756            log.debug("RampToTarget[{}]current[{}]", getTargetSpeed(), throttle.getSpeedSetting());
2757            stepQueue = new LinkedList<>();
2758            if (throttle.getSpeedSetting() == getTargetSpeed())
2759                return;
2760            else if (throttle.getSpeedSetting() < getTargetSpeed()) {
2761                // Up (accelerate)
2762                float newSpeed = throttle.getSpeedSetting();
2763                if (newSpeed < minReliableOperatingSpeed) {
2764                    stepQueue.add(new SpeedSetting(minReliableOperatingSpeed, throttleInterval));
2765                    newSpeed = minReliableOperatingSpeed;
2766                }
2767                while (newSpeed < getTargetSpeed()) {
2768                    newSpeed += speedIncrement;
2769                    if (newSpeed > getTargetSpeed()) {
2770                        newSpeed = getTargetSpeed();
2771                    }
2772                    log.trace("NewSpeedUp[{}]", newSpeed);
2773                    stepQueue.add(new SpeedSetting(newSpeed, throttleInterval));
2774                }
2775            } else {
2776                // Down (decelerate)
2777                boolean andStop = false;
2778                if (getTargetSpeed() <= 0.0f) {
2779                    andStop = true;
2780                }
2781                float newSpeed = throttle.getSpeedSetting();
2782                while (newSpeed > getTargetSpeed()) {
2783                    newSpeed -= speedIncrement;
2784                    if (newSpeed < getTargetSpeed()) {
2785                        newSpeed = getTargetSpeed();
2786                    }
2787                    log.trace("NewSpeedDown[{}]", newSpeed);
2788                    stepQueue.add(new SpeedSetting(newSpeed, throttleInterval));
2789                }
2790                if (andStop) {
2791                    stepQueue.add(new SpeedSetting(0.0f, throttleInterval));
2792                }
2793            }
2794            if (rampingTimer == null) { //If this is the first time round then kick off the speed change
2795                setNextStep();
2796            }
2797        }
2798
2799        private void finishChange() {
2800            if (rampingTimer != null) {
2801                rampingTimer.stop();
2802            }
2803            rampingTimer = null;
2804            stepQueue.clear();
2805            stepQueue = null;
2806        }
2807
2808        synchronized void setNextStep() {
2809            if (stepQueue.isEmpty()) {
2810                log.trace("Empty");
2811                finishChange();
2812                return;
2813            }
2814            SpeedSetting ss = stepQueue.getFirst();
2815            if (ss.getDuration() == 0) {
2816                log.trace("Duratiom Zero");
2817                finishChange();
2818                return;
2819            }
2820            stepQueue.removeFirst();
2821            log.trace("Set New Speed[{}]",ss.getSpeedStep());
2822            throttle.setSpeedSetting(ss.getSpeedStep());
2823            log.debug("{}: ramp step -> {}", _activeTrain.getTrainName(), ss.getSpeedStep());
2824            rampingTimer = new javax.swing.Timer(ss.getDuration(), (java.awt.event.ActionEvent e) -> {
2825                setNextStep();
2826            });
2827            rampingTimer.setRepeats(false);
2828            rampingTimer.start();
2829        }
2830
2831        private class SpeedSetting {
2832
2833            float step = 0.0f;
2834            int duration = 0;
2835
2836            SpeedSetting(float step, int duration) {
2837                this.step = step;
2838                this.duration = duration;
2839            }
2840
2841            float getSpeedStep() {
2842                return step;
2843            }
2844
2845            int getDuration() {
2846                return duration;
2847            }
2848        }
2849
2850        /**
2851         * Set the train speed directly, bypassing ramping.
2852         *
2853         * @param speed 0.0 (stop) to 1.0 (full)
2854         */
2855        public synchronized void setSpeedImmediate(float speed) {
2856            log.trace("{}: setting speed directly to {}%", _activeTrain.getTrainName(), (int) (speed * 100));
2857            stopAllTimers();
2858            targetSpeed = applyMaxThrottleAndFactor(speed);
2859            log.debug("{}: setSpeedImmediate -> {}", _activeTrain.getTrainName(), speed);
2860            throttle.setSpeedSetting(targetSpeed);
2861        }
2862
2863        /**
2864         * Check if train is moving or stopped.
2865         *
2866         * @return true if stopped; false otherwise
2867         */
2868        public synchronized boolean isStopped() {
2869            // when stopping by speed profile you must refresh the throttle speed.
2870            return throttle.getSpeedSetting() <= 0.0004f;
2871        }
2872
2873        /**
2874         * Check if train is moving at its current requested speed.
2875         *
2876         * @return true if at requested speed; false otherwise
2877         */
2878        public synchronized boolean isAtSpeed() {
2879            return java.lang.Math.abs(throttle.getSpeedSetting() - targetSpeed) <= 0.01;
2880        }
2881
2882        /**
2883         * Flag from user to end run.
2884         */
2885        public void abort() {
2886            stopAllTimers();
2887        }
2888
2889        protected void setFunction(int cmdNum, boolean isSet) {
2890            throttle.setFunction(cmdNum, isSet);
2891        }
2892        }
2893
2894        /**
2895         * Convert ramp rate name, stored as a string into the constant value
2896         * assigned.
2897         *
2898         * @param rampRate name of ramp rate, such as "RAMP_FAST"
2899         * @return integer representing a ramp rate constant value
2900         */
2901        public static int getRampRateFromName(String rampRate) {
2902            if (rampRate.equals(Bundle.getMessage("RAMP_FAST")))
2903                return RAMP_FAST;
2904            else if (rampRate.equals(Bundle.getMessage("RAMP_MEDIUM")))
2905                return RAMP_MEDIUM;
2906            else if (rampRate.equals(Bundle.getMessage("RAMP_MED_SLOW")))
2907                return RAMP_MED_SLOW;
2908            else if (rampRate.equals(Bundle.getMessage("RAMP_SLOW")))
2909                return RAMP_SLOW;
2910            else if (rampRate.equals(Bundle.getMessage("RAMP_SPEEDPROFILE")))
2911                return RAMP_SPEEDPROFILE;
2912            else if (rampRate.equals(Bundle.getMessage("RAMP_PHYSICS")))
2913                return RAMP_PHYSICS;
2914            return RAMP_NONE;
2915        }
2916
2917        /*
2918         * Listener for switching Ghost blocks to unoccupied
2919         */
2920        static class DarkTerritoryListener implements PropertyChangeListener {
2921            private Sensor sensor;
2922
2923            public DarkTerritoryListener(Sensor sensor) {
2924                this.sensor = sensor;
2925                log.trace("Sensor[{}]", sensor.getDisplayName());
2926            }
2927
2928            @Override
2929            public void propertyChange(PropertyChangeEvent e) {
2930                if (e.getPropertyName().equals("state")) {
2931                    ((Block) e.getSource()).removePropertyChangeListener(this);
2932                    if (e.getNewValue().equals(Block.UNOCCUPIED)) {
2933                        try {
2934                            log.trace("Sensor INACTIVE[{}]", sensor.getDisplayName());
2935                            sensor.setKnownState(Sensor.INACTIVE);
2936                        } catch (jmri.JmriException ex) {
2937                            log.error("Error leaving darkterratory");
2938                        }
2939                    }
2940                }
2941            }
2942        }
2943
2944    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AutoActiveTrain.class);
2945}