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