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