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