001package jmri.jmrit.logix;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import javax.annotation.Nonnull;
005
006import java.awt.Color;
007import java.util.List;
008import java.util.ListIterator;
009import jmri.DccThrottle;
010import jmri.NamedBean;
011import jmri.NamedBeanHandle;
012import jmri.Sensor;
013import jmri.util.ThreadingUtil;
014import jmri.jmrit.logix.ThrottleSetting.Command;
015import jmri.jmrit.logix.ThrottleSetting.CommandValue;
016import jmri.jmrit.logix.ThrottleSetting.ValueType;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020
021/**
022 * Execute a throttle command script for a warrant.
023 * <p>
024 * This generally operates on its own thread, but calls the warrant
025 * thread via Warrant.fireRunStatus to show status. fireRunStatus uses
026 * ThreadingUtil.runOnGUIEventually to display on the layout thread.
027 *
028 * @author Pete Cressman Copyright (C) 2009, 2010, 2020
029 */
030/*
031 * ************************ Thread running the train ****************
032 */
033class Engineer extends Thread implements java.beans.PropertyChangeListener {
034
035    private int _idxCurrentCommand;     // current throttle command
036    private ThrottleSetting _currentCommand;
037    private long _commandTime = 0;      // system time when command was executed.
038    private int _idxSkipToSpeedCommand;   // skip to this index to reset script when ramping
039    private float _normalSpeed = 0;       // current commanded throttle setting from script (unmodified)
040    // speed name of current motion. When train stopped, is the speed that will be restored when movement is permitted
041    private String _speedType = Warrant.Normal; // is never Stop or EStop
042    private float _timeRatio = 1.0f;     // ratio to extend scripted time when speed is modified
043    private boolean _abort = false;
044    private boolean _halt = false;  // halt/resume from user's control
045    private boolean _stopPending = false;   // ramp slow down in progress
046    private boolean _waitForClear = false;  // waits for signals/occupancy/allocation to clear
047    private boolean _waitForSensor = false; // wait for sensor event
048    private boolean _runOnET = false;   // Execute commands on ET only - do not synch
049    private boolean _setRunOnET = false; // Need to delay _runOnET from the block that set it
050    protected DccThrottle _throttle;
051    private final Warrant _warrant;
052    private final List<ThrottleSetting> _commands;
053    private Sensor _waitSensor;
054    private int _sensorWaitState;
055    private Object _rampLockObject = new Object();
056    private Object _synchLockObject = new Object();
057    private Object _clearLockObject = new Object();
058    private boolean _atHalt = false;
059    private boolean _atClear = false;
060    private final SpeedUtil _speedUtil;
061    private OBlock _synchBlock = null;
062    private Thread _checker = null;
063
064    private ThrottleRamp _ramp;
065    private boolean _holdRamp = false;
066    private boolean _isRamping = false;
067
068    Engineer(Warrant warrant, DccThrottle throttle) {
069        _warrant = warrant;
070        _throttle = throttle;
071        _speedUtil = warrant.getSpeedUtil();
072        _commands = _warrant.getThrottleCommands();
073        _idxCurrentCommand = 0;
074        _currentCommand = _commands.get(_idxCurrentCommand);
075        _idxSkipToSpeedCommand = 0;
076        _waitForSensor = false;
077        setName("Engineer(" + _warrant.getTrainName() +")");
078    }
079
080    @Override
081    @SuppressFBWarnings(value="UW_UNCOND_WAIT", justification="waits may be indefinite until satisfied or thread aborted")
082    public void run() {
083        if (log.isDebugEnabled())
084            log.debug("Engineer started warrant {} _throttle= {}", _warrant.getDisplayName(), _throttle.getClass().getName());
085
086        int cmdBlockIdx = 0;
087        while (_idxCurrentCommand < _commands.size()) {
088            while (_idxSkipToSpeedCommand > _idxCurrentCommand) {
089                if (log.isDebugEnabled()) {
090                    ThrottleSetting ts = _commands.get(_idxCurrentCommand);
091                    log.debug("{}: Skip Cmd #{}: {} Warrant", _warrant.getDisplayName(), _idxCurrentCommand+1, ts);
092                    // Note: command indexes biased from 0 to 1 to match Warrant display of commands.
093                }
094                _idxCurrentCommand++;
095            }
096            if (_idxCurrentCommand == _commands.size()) {
097                // skip commands on last block may advance too far. Due to looking for a NOOP
098                break;
099            }
100            _currentCommand = _commands.get(_idxCurrentCommand);
101            long cmdWaitTime = _currentCommand.getTime();    // time to wait before executing command
102            ThrottleSetting.Command command = _currentCommand.getCommand();
103            _runOnET = _setRunOnET;     // OK to set here
104            if (command.hasBlockName()) {
105                int idx = _warrant.getIndexOfBlockAfter((OBlock)_currentCommand.getBean(), cmdBlockIdx);
106                if (idx >= 0) {
107                    cmdBlockIdx = idx;
108                }
109            }
110            if (cmdBlockIdx < _warrant.getCurrentOrderIndex() ||
111                    (command.equals(Command.NOOP) && (cmdBlockIdx <= _warrant.getCurrentOrderIndex()))) {
112                // Train advancing too fast, need to process commands more quickly,
113                // allow some time for whistle toots etc.
114                cmdWaitTime = Math.min(cmdWaitTime, 200); // 200ms per command should be enough for toots etc.
115                if (log.isDebugEnabled())
116                    log.debug("{}: Train reached block \"{}\" before script et={}ms",
117                            _warrant.getDisplayName(), _warrant.getCurrentBlockName(), _currentCommand.getTime());
118            }
119            if (_abort) {
120                break;
121            }
122
123            long cmdStart = System.currentTimeMillis();
124            if (log.isDebugEnabled())
125                log.debug("{}: Start Cmd #{} for block \"{}\" currently in \"{}\". wait {}ms to do cmd {}",
126                    _warrant.getDisplayName(), _idxCurrentCommand+1, _currentCommand.getBeanDisplayName(),
127                    _warrant.getCurrentBlockName(), cmdWaitTime, command.toString());
128                    // Note: command indexes biased from 0 to 1 to match Warrant display of commands.
129            synchronized (this) {
130                if (!Warrant.Normal.equals(_speedType)) {
131                    cmdWaitTime = (long)(cmdWaitTime*_timeRatio); // extend et when speed has been modified from scripted speed
132                }
133                try {
134                    if (cmdWaitTime > 0) {
135                        wait(cmdWaitTime);
136                    }
137                } catch (InterruptedException ie) {
138                    log.debug("InterruptedException during time wait", ie);
139                    _warrant.debugInfo();
140                    Thread.currentThread().interrupt();
141                    _abort = true;
142                } catch (java.lang.IllegalArgumentException iae) {
143                    log.error("At time wait", iae);
144                }
145            }
146            if (_abort) {
147                break;
148            }
149
150            // Having waited, time=ts.getTime(), so blocks should agree.  if not,
151            // wait for train to arrive at block and send sync notification.
152            // note, blind runs cannot detect entrance.
153            if (!_runOnET && cmdBlockIdx > _warrant.getCurrentOrderIndex()) {
154                // commands are ahead of current train position
155                // When the next block goes active or a control command is made, a clear sync call
156                // will test these indexes again and can trigger a notify() to free the wait
157
158                synchronized (_synchLockObject) {
159                    _synchBlock = _warrant.getBlockAt(cmdBlockIdx);
160                    _warrant.fireRunStatus("WaitForSync", _idxCurrentCommand - 1, _idxCurrentCommand);
161                    if (log.isDebugEnabled()) {
162                        log.debug("{}: Wait for train to enter \"{}\".",
163                                _warrant.getDisplayName(), _synchBlock.getDisplayName());
164                    }
165                    try {
166                        _synchLockObject.wait();
167                        _synchBlock = null;
168                    } catch (InterruptedException ie) {
169                        log.debug("InterruptedException during _waitForSync", ie);
170                        _warrant.debugInfo();
171                        Thread.currentThread().interrupt();
172                        _abort = true;
173                    }
174                }
175                if (_abort) {
176                    break;
177                }
178            }
179
180            synchronized (_clearLockObject) {
181                // block position and elapsed time are as expected, but track conditions
182                // such as signals or rogue occupancy requires waiting
183                if (_waitForClear) {
184                    try {
185                        _atClear = true;
186                        if (log.isDebugEnabled())
187                            log.debug("{}: Waiting for clearance. _waitForClear= {} _halt= {} at block \"{}\" Cmd#{}.",
188                                _warrant.getDisplayName(), _waitForClear, _halt,
189                                _warrant.getBlockAt(cmdBlockIdx).getDisplayName(), _idxCurrentCommand+1);
190                        _clearLockObject.wait();
191                        _waitForClear = false;
192                        _atClear = false;
193                    } catch (InterruptedException ie) {
194                        log.debug("InterruptedException during _atClear", ie);
195                        _warrant.debugInfo();
196                        Thread.currentThread().interrupt();
197                        _abort = true;
198                    }
199                }
200            }
201            if (_abort) {
202                break;
203            }
204
205            synchronized (this) {
206                // user's command to halt requires waiting
207                if (_halt) {
208                    try {
209                        _atHalt = true;
210                        if (log.isDebugEnabled())
211                            log.debug("{}: Waiting to Resume. _halt= {}, _waitForClear= {}, Block \"{}\".",
212                                _warrant.getDisplayName(), _halt, _waitForClear,
213                                _warrant.getBlockAt(cmdBlockIdx).getDisplayName());
214                        wait();
215                        _halt = false;
216                        _atHalt = false;
217                    } catch (InterruptedException ie) {
218                        log.debug("InterruptedException during _atHalt", ie);
219                        _warrant.debugInfo();
220                        Thread.currentThread().interrupt();
221                        _abort = true;
222                    }
223                }
224            }
225            if (_abort) {
226                break;
227            }
228
229            synchronized (this) {
230                while (_isRamping || _holdRamp) {
231                    int idx = _idxCurrentCommand;
232                    try {
233                        if (log.isDebugEnabled())
234                            log.debug("{}: Waiting for ramp to finish at Cmd #{}.",
235                                  _warrant.getDisplayName(), _idxCurrentCommand+1);
236                        wait();
237                    } catch (InterruptedException ie) {
238                        _warrant.debugInfo();
239                        Thread.currentThread().interrupt();
240                        _abort = true;
241                    }
242                    // ramp will decide whether to skip or execute _currentCommand
243                    if (log.isDebugEnabled()) {
244                        log.debug("{}: Cmd #{} held for {}ms. {}", _warrant.getDisplayName(),
245                                idx+1, System.currentTimeMillis() - cmdStart, _currentCommand);
246                    }
247                }
248                if (_idxSkipToSpeedCommand <= _idxCurrentCommand) {
249                    executeComand(_currentCommand, System.currentTimeMillis() - cmdStart);
250                    _idxCurrentCommand++;
251                }
252            }
253        }
254        // shut down
255        setSpeed(0.0f); // for safety to be sure train stops
256        _warrant.stopWarrant(_abort, true);
257    }
258
259    private void executeComand(ThrottleSetting ts, long et) {
260        Command command = ts.getCommand();
261        CommandValue cmdVal = ts.getValue();
262        switch (command) {
263            case SPEED:
264                _normalSpeed = cmdVal.getFloat();
265                float speedMod = _speedUtil.modifySpeed(_normalSpeed, _speedType);
266                if (_normalSpeed > speedMod) {
267                    float trackSpeed = _speedUtil.getTrackSpeed(speedMod);
268                    _timeRatio = _speedUtil.getTrackSpeed(_normalSpeed) / trackSpeed;
269                    _speedUtil.speedChange(speedMod);  // call before this setting to compute travel of last setting
270                    setSpeed(speedMod);
271                } else {
272                    _timeRatio = 1.0f;
273                    _speedUtil.speedChange(_normalSpeed);  // call before this setting to compute travel of last setting
274                    setSpeed(_normalSpeed);
275                }
276                break;
277            case NOOP:
278                break;
279            case SET_SENSOR:
280                ThreadingUtil.runOnGUIEventually(() ->
281                    setSensor(ts.getNamedBeanHandle(), cmdVal));
282                break;
283            case FKEY:
284                setFunction(ts.getKeyNum(), cmdVal.getType());
285                break;
286            case FORWARD:
287                setForward(cmdVal.getType());
288                break;
289            case LATCHF:
290                setFunctionMomentary(ts.getKeyNum(), cmdVal.getType());
291                break;
292            case WAIT_SENSOR:
293                waitForSensor(ts.getNamedBeanHandle(), cmdVal);
294                break;
295            case RUN_WARRANT:
296                ThreadingUtil.runOnGUIEventually(() ->
297                    runWarrant(ts.getNamedBeanHandle(), cmdVal));
298                break;
299            case SPEEDSTEP:
300                break;
301            case SET_MEMORY:
302                ThreadingUtil.runOnGUIEventually(() ->
303                    setMemory(ts.getNamedBeanHandle(), cmdVal));
304                break;
305            default:
306        }
307        _commandTime = System.currentTimeMillis();
308        if (log.isDebugEnabled()) {
309            log.debug("{}: Cmd #{} done. et={}. {}",
310                   _warrant.getDisplayName(), _idxCurrentCommand + 1, et, ts);
311        }
312    }
313
314    protected int getCurrentCommandIndex() {
315        return _idxCurrentCommand;
316    }
317
318    /**
319     * Delayed ramp has started.
320     * Currently informational only
321     * Do non-speed commands only until idx is reached?  maybe not.
322     * @param idx index
323     */
324    private void advanceToCommandIndex(int idx) {
325        _idxSkipToSpeedCommand = idx;
326      if (log.isTraceEnabled())
327            log.debug("advanceToCommandIndex to {} - {}", _idxSkipToSpeedCommand+1, _commands.get(idx));
328            // Note: command indexes biased from 0 to 1 to match Warrant display of commands, which are 1-based.
329    }
330
331    /**
332     * Cannot set _runOnET to true until current NOOP command completes
333     * so there is the intermediate flag _setRunOnET
334     * @param set true to run on elapsed time calculations only, false to
335     *            consider other inputs
336     */
337    protected void setRunOnET(boolean set) {
338        if (log.isDebugEnabled() && _setRunOnET != set) {
339            log.debug("{}: setRunOnET {} command #{}", _warrant.getDisplayName(),
340                    set, _idxCurrentCommand+1);
341            // Note: command indexes biased from 0 to 1 to match Warrant display of commands.
342        }
343        _setRunOnET = set;
344        if (!set) { // OK to be set false immediately
345            _runOnET = false;
346        }
347    }
348
349    protected boolean getRunOnET() {
350        return _setRunOnET;
351    }
352
353    protected OBlock getSynchBlock() {
354        return _synchBlock;
355    }
356    /**
357     * Called by the warrant when a the block ahead of a moving train goes occupied.
358     * typically when this thread is on a timed wait. The call will free the wait.
359     * @param block going active.
360     */
361    protected void clearWaitForSync(OBlock block) {
362        // block went active. if waiting on sync, clear it
363        if (_synchBlock != null) {
364            synchronized (_synchLockObject) {
365                if (block.equals(_synchBlock)) {
366                    _synchLockObject.notifyAll();
367                    if (log.isDebugEnabled()) {
368                        log.debug("{}: clearWaitForSync from block \"{}\". notifyAll() called.  isRamping()={}",
369                                _warrant.getDisplayName(), block.getDisplayName(), isRamping());
370                    }
371                    return;
372                }
373            }
374        }
375        if (log.isDebugEnabled()) {
376            log.debug("{}: clearWaitForSync from block \"{}\". _synchBlock= {} SpeedState={} _atClear={} _atHalt={}",
377                    _warrant.getDisplayName(), block.getDisplayName(),
378                    (_synchBlock==null?"null":_synchBlock.getDisplayName()), getSpeedState(), _atClear, _atHalt);
379        }
380    }
381
382    private static void setFrameStatusText(String m, Color c, boolean save) {
383        ThreadingUtil.runOnGUIEventually(()-> WarrantTableFrame.getDefault().setStatusText(m, c, true));
384    }
385
386    /**
387     * Occupancy of blocks, user halts and aspects of Portal signals will modify
388     * normal scripted train speeds.
389     * Ramp speed change for smooth prototypical look.
390     *
391     * @param endSpeedType signal aspect speed name
392     * @param endBlockIdx BlockOrder index of the block where ramp is to end.
393     *        -1 if an end block is not specified.
394     */
395    @SuppressFBWarnings(value="SLF4J_FORMAT_SHOULD_BE_CONST", justification="False assumption")
396    protected synchronized void rampSpeedTo(@Nonnull String endSpeedType, int endBlockIdx) {
397        float speed = _speedUtil.modifySpeed(_normalSpeed, endSpeedType);
398        if (log.isDebugEnabled()) {
399            log.debug("{}: rampSpeedTo: type= {}, throttle from {} to {}.",
400                _warrant.getDisplayName(), endSpeedType, getSpeedSetting(),
401                speed);
402        }
403        _speedUtil.speedChange(-1); // Notify not to measure speed for speedProfile
404        if (endSpeedType.equals(Warrant.EStop)) {
405            setStop(true);
406            return;
407        }
408        if (endSpeedType.equals(Warrant.Stop) && getSpeedSetting() <= 0) {
409            setStop(false);
410            return; // already stopped, do nothing
411        }
412        if (_isRamping) {
413            if (endSpeedType.equals(_ramp._endSpeedType)) {
414                return; // already ramping to speedType
415            }
416        } else if (speed == getSpeedSetting()){
417            // to be sure flags and notification is done
418            rampDone(false, endSpeedType, endBlockIdx);
419            return; // already at speedType speed
420        }
421        if (_ramp == null) {
422            _ramp = new ThrottleRamp();
423            _ramp.start();
424        } else if (_isRamping) {
425            // for repeated command already ramping
426            if (_ramp.duplicate(endSpeedType, endBlockIdx)) {
427                return;
428            }
429            // stop the ramp and replace it
430            _holdRamp = true;
431            _ramp.quit(false);
432        }
433        long time = 0;
434        int pause = 2 *_speedUtil.getRampTimeIncrement();
435        do {
436            // may need a bit of time for quit() or start() to get ready
437            try {
438                wait(40);
439                time += 40;
440                _ramp.quit(false);
441            }
442            catch (InterruptedException ie) { // ignore
443            }
444        } while (time <= pause && _isRamping);
445
446        if (!_isRamping) {
447            if (Warrant._trace || log.isDebugEnabled()) {
448                log.info(Bundle.getMessage("RampStart", _warrant.getTrainName(),
449                        endSpeedType, _warrant.getCurrentBlockName()));
450            }
451            _ramp.setParameters(endSpeedType, endBlockIdx);
452            synchronized (_rampLockObject) {
453                _ramp._rampDown = (endBlockIdx >= 0) || endSpeedType.equals(Warrant.Stop);
454//                setIsRamping(true);
455                _holdRamp = false;
456                setWaitforClear(true);
457                _rampLockObject.notifyAll(); // free wait at ThrottleRamp.run()
458                log.debug("{}: rampSpeedTo calls notify _rampLockObject", _warrant.getDisplayName());
459            }
460        } else {
461            log.error("Can't launch ramp for speed {}! _ramp Thread.State= {}. Waited {}ms",
462                    endSpeedType, _ramp.getState(), time);
463            _warrant.debugInfo();
464            setSpeedToType(endSpeedType);
465            _ramp.quit(true);
466            _ramp.interrupt();
467            _ramp = null;
468        }
469    }
470
471    protected boolean isRamping() {
472        return _isRamping;
473    }
474    private void setIsRamping(boolean set) {
475        _isRamping = set;
476    }
477
478    /**
479     * Get the Speed type name. _speedType is the type when moving. Used to restore
480     * speeds aspects of signals when halts or other conditions have stopped the train.
481     * If 'absolute' is true return the absolute speed of the train, i.e. 'Stop' if
482     * train is not moving.
483     * @param absolute  which speed type, absolute or allowed movement
484     * @return speed type
485     */
486    protected String getSpeedType(boolean absolute) {
487        if (absolute) {
488            if (isRamping()) {   // return pending type
489                return _ramp._endSpeedType;
490            }
491            if (_waitForClear || _halt) {
492                return Warrant.Stop;
493            }
494        }
495        return _speedType;
496    }
497
498    /*
499     * warrant.cancelDelayRamp()  called for immediate Stop commands
500     * When die==true for ending the warrant run.
501     */
502    synchronized protected boolean cancelRamp(boolean die) {
503        // _ramp.quit sets "stop" and notifies "waits"
504        if (_ramp != null) {
505            if (die) {
506                _ramp.quit(true);
507                _ramp.interrupt();
508            } else {
509                if(_isRamping) {
510                    _ramp.quit(false);
511                    return true;
512                }
513            }
514        }
515        return false;
516    }
517
518    /**
519     * do throttle setting
520     * @param speed throttle setting about to be set. Modified to sType if from script.
521     * UnModified if from ThrottleRamp or stop speeds.
522     */
523     protected void setSpeed(float speed) {
524        _throttle.setSpeedSetting(speed);
525        // Late update to GUI is OK, this is just an informational status display
526        if (!_abort) {
527            _warrant.fireRunStatus("SpeedChange", null, null);
528        }
529        if (log.isDebugEnabled())
530            log.debug("{}: _throttle.setSpeedSetting({}) called, ({}).",
531                    _warrant.getDisplayName(), speed, _speedType);
532    }
533
534    protected float getSpeedSetting() {
535        float speed = _throttle.getSpeedSetting();
536        if (speed < 0.0f) {
537            _throttle.setSpeedSetting(0.0f);
538            speed = _throttle.getSpeedSetting();
539        }
540        return speed;
541    }
542
543    protected float getScriptSpeed() {
544        return _normalSpeed;
545    }
546
547    /**
548     * Utility for unscripted speed changes.
549     * Records current type and sets time ratio.
550     * @param speedType name of speed change type
551     */
552    private void setSpeedRatio(String speedType) {
553        if (speedType.equals(Warrant.Normal)) {
554            _timeRatio = 1.0f;
555        } else if (_normalSpeed > 0.0f) {
556            float speedMod = _speedUtil.modifySpeed(_normalSpeed, _speedType);
557            if (_normalSpeed > speedMod) {
558                float trackSpeed = _speedUtil.getTrackSpeed(speedMod);
559                _timeRatio = _speedUtil.getTrackSpeed(_normalSpeed) / trackSpeed;
560            } else {
561                _timeRatio = 1.0f;
562            }
563        } else {
564            _timeRatio = 1.0f;
565        }
566    }
567
568    /*
569     * Do immediate speed change.
570     */
571    protected synchronized void setSpeedToType(String speedType) {
572        float speed = getSpeedSetting();
573        if (log.isDebugEnabled())  {
574            log.debug("{}: setSpeedToType({}) speed={} scriptSpeed={}", _warrant.getDisplayName(), speedType, speed, _normalSpeed);
575        }
576        if (speedType.equals(Warrant.Stop)) {
577            setStop(false);
578            advanceToCommandIndex(_idxCurrentCommand + 1);  // skip current command
579        } else if (speedType.equals(Warrant.EStop)) {
580            setStop(true);
581            advanceToCommandIndex(_idxCurrentCommand + 1);  // skip current command
582        } else if (speedType.equals(getSpeedType(true))) {
583            return;
584        } else {
585            _speedType = speedType;     // set speedType regardless
586            setSpeedRatio(speedType);
587            float speedMod = _speedUtil.modifySpeed(_normalSpeed, _speedType);
588            _speedUtil.speedChange(speedMod);  // call before this setting to compute travel of last setting
589            setSpeed(speedMod);
590        }
591    }
592
593    /**
594     * Command to stop (or resume speed) of train from Warrant.controlRunTrain()
595     * of user's override of throttle script.  Also from error conditions
596     * such as losing detection of train's location.
597     * @param halt true if train should halt
598     */
599    protected synchronized void setHalt(boolean halt) {
600        if (log.isDebugEnabled())
601            log.debug("{}: setHalt({}): _atHalt= {}, _waitForClear= {}",
602                  _warrant.getDisplayName(), halt, _atHalt, _waitForClear);
603        if (!halt) {    // resume normal running
604            _halt = false;
605            if (!_atClear) {
606                log.debug("setHalt calls notify()");
607                notifyAll();   // free wait at _atHalt
608            }
609        } else {
610            _halt = true;
611        }
612    }
613
614    private long getTimeToNextCommand() {
615        if (_commandTime > 0) {
616            // millisecs already moving on pending command's time.
617            long elapsedTime = System.currentTimeMillis() - _commandTime;
618            return Math.max(0, (_currentCommand.getTime() - elapsedTime));
619        }
620        return 0;
621    }
622
623    /**
624     * Command to stop or smoothly resume speed. Stop due to
625     * signal or occupation stopping condition ahead.  Caller
626     * follows with call for type of stop to make.
627     * Track condition override of throttle script.
628     * @param wait true if train should stop
629     */
630    protected void setWaitforClear(boolean wait) {
631        if (log.isDebugEnabled())
632            log.debug("{}: setWaitforClear({}): _atClear= {}, throttle speed= {}, _halt= {}",
633                   _warrant.getDisplayName(), wait, _atClear,  getSpeedSetting(), _halt);
634        if (!wait) {    // resume normal running
635            synchronized (_clearLockObject) {
636                log.debug("setWaitforClear calls notify");
637                _waitForClear = false;
638                _clearLockObject.notifyAll();   // free wait at _atClear
639            }
640        } else {
641            _waitForClear = true;
642        }
643    }
644
645    String debugInfo() {
646        StringBuffer info = new StringBuffer("\n");
647        info.append(getName()); info.append(" on warrant= "); info.append(_warrant.getDisplayName());
648        info.append("\nThread.State= "); info.append(getState());
649        info.append(", isAlive= "); info.append(isAlive());
650        info.append(", isInterrupted= "); info.append(isInterrupted());
651        info.append("\n\tThrottle setting= "); info.append(getSpeedSetting());
652        info.append(", scriptSpeed= "); info.append(getScriptSpeed());
653        info.append(". runstate= "); info.append(Warrant.RUN_STATE[getRunState()]);
654        int cmdIdx = getCurrentCommandIndex();
655
656        if (cmdIdx < _commands.size()) {
657            info.append("\n\tCommand #"); info.append(cmdIdx + 1);
658            info.append(": "); info.append(_commands.get(cmdIdx).toString());
659        } else {
660            info.append("\n\t\tAt last command.");
661        }
662        // Note: command indexes biased from 0 to 1 to match Warrant's 1-based display of commands.
663        info.append("\n\tEngineer flags: _waitForClear= "); info.append(_waitForClear);
664        info.append(", _atclear= "); info.append(_atClear);
665        info.append(", _halt= "); info.append(_halt);
666        info.append(", _atHalt= "); info.append(_atHalt);
667        if (_synchBlock != null) {
668            info.append("\n\t\tWaiting for Sync at \"");info.append(_synchBlock.getDisplayName());
669        }
670        info.append("\"\n\t\t_setRunOnET= "); info.append(_setRunOnET);
671        info.append(", _runOnET= "); info.append(_runOnET);
672        info.append("\n\t\t_stopPending= "); info.append(_stopPending);
673        info.append(", _abort= "); info.append(_abort);
674        info.append("\n\t_speedType= \""); info.append(_speedType); info.append("\" SpeedState= ");
675        info.append(getSpeedState().toString()); info.append("\n\tStack trace:");
676        for (StackTraceElement elem : getStackTrace()) {
677            info.append("\n\t\t");
678            info.append(elem.getClassName()); info.append("."); info.append(elem.getMethodName());
679            info.append(", line "); info.append(elem.getLineNumber());
680        }
681        if (_ramp != null) {
682            info.append("\n\tRamp Thread.State= "); info.append(_ramp.getState());
683            info.append(", isAlive= "); info.append(_ramp.isAlive());
684            info.append(", isInterrupted= "); info.append(_ramp.isInterrupted());
685            info.append("\n\tRamp flags: _isRamping= "); info.append(_isRamping);
686            info.append(", stop= "); info.append(_ramp.stop);
687            info.append(", _die= "); info.append(_ramp._die);
688            info.append("\n\tRamp Type: "); info.append(_ramp._rampDown ? "DOWN" : "UP");info.append(" ramp");
689            info.append("\n\t\tEndSpeedType= \""); info.append(_ramp._endSpeedType);
690            int endIdx = _ramp.getEndBlockIndex();
691            info.append("\"\n\t\tEndBlockIdx= "); info.append(endIdx);
692            if (endIdx >= 0) {
693                info.append(" EndBlock= \"");
694                info.append(_warrant.getBlockAt(endIdx).getDisplayName());
695            }
696            info.append("\""); info.append("\n\tStack trace:");
697            for (StackTraceElement elem : _ramp.getStackTrace()) {
698                info.append("\n\t\t");
699                info.append(elem.getClassName()); info.append("."); info.append(elem.getMethodName());
700                info.append(", line "); info.append(elem.getLineNumber());
701            }
702        } else {
703            info.append("\n\tNo ramp.");
704        }
705        return info.toString();
706    }
707
708    /**
709     * Immediate stop command from Warrant.controlRunTrain()-user
710     * or from Warrant.goingInactive()-train lost
711     * or from setMovement()-overrun, possible collision risk.
712     * Do not ramp.
713     * @param eStop true for emergency stop
714     */
715    private synchronized void setStop(boolean eStop) {
716        float speed = _throttle.getSpeedSetting();
717        if (speed <= 0.0f && (_waitForClear || _halt)) {
718            return;
719        }
720        cancelRamp(false);
721        if (eStop) {
722            setHalt(true);
723            setSpeed(-0.1f);
724            setSpeed(0.0f);
725        } else {
726            setSpeed(0.0f);
727            setWaitforClear(true);
728        }
729        if (log.isDebugEnabled())
730            log.debug("{}: setStop({}) from speed={} scriptSpeed={}", _warrant.getDisplayName(), eStop, speed, _normalSpeed);
731    }
732
733    protected Warrant.SpeedState getSpeedState() {
734        if (isRamping()) {
735            if (_ramp._rampDown) {
736                return Warrant.SpeedState.RAMPING_DOWN;
737            } else {
738                return Warrant.SpeedState.RAMPING_UP;
739            }
740        }
741        return Warrant.SpeedState.STEADY_SPEED;
742    }
743
744    protected int getRunState() {
745        if (_stopPending) {
746            if (_halt) {
747                return Warrant.RAMP_HALT;
748            }
749            return Warrant.STOP_PENDING;
750        } else if (_halt) {
751            return Warrant.HALT;
752        } else if (_waitForClear) {
753            return Warrant.WAIT_FOR_CLEAR;
754        } else if (_waitForSensor) {
755            return Warrant.WAIT_FOR_SENSOR;
756        } else if (_abort) {
757            return Warrant.ABORT;
758        } else if (_synchBlock != null) {
759            return Warrant.WAIT_FOR_TRAIN;
760        } else if (isRamping()) {
761            return Warrant.SPEED_RESTRICTED;
762        }
763        return Warrant.RUNNING;
764    }
765
766    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY", justification="quit is called another thread to clear all ramp waits")
767    public void stopRun(boolean abort, boolean turnOffFunctions) {
768        if (abort) {
769            _abort =true;
770        }
771
772        synchronized (_synchLockObject) {
773            _synchLockObject.notifyAll();
774        }
775        synchronized (_clearLockObject) {
776            _clearLockObject.notifyAll();
777        }
778        synchronized (this) {
779            notifyAll();
780        }
781
782        cancelRamp(true);
783        if (_waitSensor != null) {
784            _waitSensor.removePropertyChangeListener(this);
785        }
786
787        if (_throttle != null) {
788            if (_throttle.getSpeedSetting() > 0.0f) {
789                if (abort) {
790                    _throttle.setSpeedSetting(-1.0f);
791                }
792                setSpeed(0.0f);
793                if (turnOffFunctions) {
794                    _throttle.setFunction(0, false);
795                    _throttle.setFunction(1, false);
796                    _throttle.setFunction(2, false);
797                    _throttle.setFunction(3, false);
798                }
799            }
800            _warrant.releaseThrottle(_throttle);
801        }
802    }
803
804    private void setForward(ValueType type) {
805        if (type == ValueType.VAL_TRUE) {
806            _throttle.setIsForward(true);
807        } else if (type == ValueType.VAL_FALSE) {
808            _throttle.setIsForward(false);
809        } else {
810            throw new java.lang.IllegalArgumentException("setForward type " + type + " wrong");
811        }
812    }
813
814    private void setFunction(int cmdNum, ValueType type) {
815        if ( cmdNum < 0 || cmdNum > 28 ) {
816            throw new java.lang.IllegalArgumentException("setFunction " + cmdNum + " out of range");
817        }
818        if (type == ValueType.VAL_ON) {
819            _throttle.setFunction(cmdNum, true);
820        } else if (type == ValueType.VAL_OFF) {
821            _throttle.setFunction(cmdNum,false);
822        } else {
823            throw new java.lang.IllegalArgumentException("setFunction type " + type + " wrong");
824        }
825    }
826
827    private void setFunctionMomentary(int cmdNum, ValueType type) {
828        if ( cmdNum < 0 || cmdNum > 28 ) {
829            log.error("Function value {} out of range",cmdNum);
830            throw new java.lang.IllegalArgumentException("setFunctionMomentary " + cmdNum + " out of range");
831        }
832        if (type == ValueType.VAL_ON) {
833            _throttle.setFunctionMomentary(cmdNum, true);
834        } else if (type == ValueType.VAL_OFF) {
835            _throttle.setFunctionMomentary(cmdNum,false);
836        } else {
837            throw new java.lang.IllegalArgumentException("setFunctionMomentary type " + type + " wrong");
838        }
839    }
840
841    /**
842     * Set Memory value
843     */
844    private void setMemory(NamedBeanHandle<?> handle, CommandValue cmdVal) {
845        NamedBean bean = handle.getBean();
846        if (!(bean instanceof jmri.Memory)) {
847            log.error("setMemory: {} not a Memory!", bean );
848            return;
849        }
850        jmri.Memory m = (jmri.Memory)bean;
851        ValueType type = cmdVal.getType();
852
853        if (Warrant._trace || log.isDebugEnabled()) {
854            log.info("{} : Set memory", Bundle.getMessage("setMemory",
855                        _warrant.getTrainName(), m.getDisplayName(), cmdVal.getText()));
856        }
857        _warrant.fireRunStatus("MemorySetCommand", type.toString(), m.getDisplayName());
858        m.setValue(cmdVal.getText());
859    }
860
861    /**
862     * Set Sensor state
863     */
864    private void setSensor(NamedBeanHandle<?> handle, CommandValue cmdVal) {
865        NamedBean bean = handle.getBean();
866        if (!(bean instanceof Sensor)) {
867            log.error("setSensor: {} not a Sensor!", bean );
868            return;
869        }
870        jmri.Sensor s = (Sensor)bean;
871        ValueType type = cmdVal.getType();
872        try {
873            if (Warrant._trace || log.isDebugEnabled()) {
874                log.info("{} : Set Sensor", Bundle.getMessage("setSensor",
875                            _warrant.getTrainName(), s.getDisplayName(), type.toString()));
876            }
877            _warrant.fireRunStatus("SensorSetCommand", type.toString(), s.getDisplayName());
878            if (type == ValueType.VAL_ACTIVE) {
879                s.setKnownState(jmri.Sensor.ACTIVE);
880            } else if (type == ValueType.VAL_INACTIVE) {
881                s.setKnownState(jmri.Sensor.INACTIVE);
882            } else {
883                throw new java.lang.IllegalArgumentException("setSensor type " + type + " wrong");
884            }
885        } catch (jmri.JmriException e) {
886            log.warn("Exception setting sensor {} in action", handle.toString());
887        }
888    }
889
890    /**
891     * Wait for Sensor state event
892     */
893    private void waitForSensor(NamedBeanHandle<?> handle, CommandValue cmdVal) {
894        if (_waitSensor != null) {
895            _waitSensor.removePropertyChangeListener(this);
896        }
897        NamedBean bean = handle.getBean();
898        if (!(bean instanceof Sensor)) {
899            log.error("setSensor: {} not a Sensor!", bean );
900            return;
901        }
902        _waitSensor = (Sensor)bean;
903        ThrottleSetting.ValueType type = cmdVal.getType();
904        if (type == ValueType.VAL_ACTIVE) {
905            _sensorWaitState = Sensor.ACTIVE;
906        } else if (type == ValueType.VAL_INACTIVE) {
907            _sensorWaitState = Sensor.INACTIVE;
908        } else {
909            throw new java.lang.IllegalArgumentException("waitForSensor type " + type + " wrong");
910        }
911        int state = _waitSensor.getKnownState();
912        if (state == _sensorWaitState) {
913            log.info("Engineer: state of event sensor {} already at state {}", _waitSensor.getDisplayName(), type.toString());
914            return;
915        }
916        _waitSensor.addPropertyChangeListener(this);
917        if (log.isDebugEnabled())
918            log.debug("Listen for propertyChange of {}, wait for State= {}", _waitSensor.getDisplayName(), _sensorWaitState);
919        // suspend commands until sensor changes state
920        synchronized (this) {   // DO NOT USE _waitForSensor for synch
921            _waitForSensor = true;
922            while (_waitForSensor) {
923                try {
924                    if (Warrant._trace || log.isDebugEnabled()) {
925                        log.info("{} : waitSensor", Bundle.getMessage("waitSensor",
926                            _warrant.getTrainName(), _waitSensor.getDisplayName(), type.toString()));
927                    }
928                    _warrant.fireRunStatus("SensorWaitCommand", type.toString(), _waitSensor.getDisplayName());
929                    wait();
930                    if (!_abort ) {
931                        String name =  _waitSensor.getDisplayName();    // save name, _waitSensor will be null 'eventually'
932                        if (Warrant._trace || log.isDebugEnabled()) {
933                            log.info("{} : wait Sensor Change", Bundle.getMessage("waitSensorChange",
934                                    _warrant.getTrainName(), name));
935                        }
936                        _warrant.fireRunStatus("SensorWaitCommand", null, name);
937                    }
938                } catch (InterruptedException ie) {
939                    log.error("Engineer interrupted at waitForSensor \"{}\"", _waitSensor.getDisplayName(), ie);
940                    _warrant.debugInfo();
941                    Thread.currentThread().interrupt();
942                } finally {
943                    clearSensor();
944                }
945            }
946        }
947    }
948
949    private void clearSensor() {
950        if (_waitSensor != null) {
951            _waitSensor.removePropertyChangeListener(this);
952        }
953        _sensorWaitState = 0;
954        _waitForSensor = false;
955        _waitSensor = null;
956    }
957
958    protected Sensor getWaitSensor() {
959        return _waitSensor;
960    }
961
962    @Override
963    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY", justification="Sensor change on another thread is expected even when Engineer (this) has not done any modifing")
964    public void propertyChange(java.beans.PropertyChangeEvent evt) {
965        if (log.isDebugEnabled())
966            log.debug("propertyChange {} new value= {}", evt.getPropertyName(), evt.getNewValue());
967        if ((evt.getPropertyName().equals("KnownState")
968                && ((Number) evt.getNewValue()).intValue() == _sensorWaitState)) {
969            synchronized (this) {
970                    notifyAll();  // free sensor wait
971            }
972        }
973    }
974
975    private void runWarrant(NamedBeanHandle<?> handle, CommandValue cmdVal) {
976        NamedBean bean = handle.getBean();
977        if (!(bean instanceof Warrant)) {
978            log.error("runWarrant: {} not a warrant!", bean );
979            return;
980        }
981        Warrant warrant =  (Warrant)bean;
982
983        int num = Math.round(cmdVal.getFloat());    // repeated loops
984        if (num == 0) {
985            return;
986        }
987        if (num > 0) { // do the countdown for all linked warrants.
988            num--;  // decrement loop count
989            cmdVal.setFloat(num);
990        }
991
992        if (_warrant.getSpeedUtil().getDccAddress().equals(warrant.getSpeedUtil().getDccAddress())) {
993            // Same loco, perhaps different warrant
994            if (log.isDebugEnabled()) {
995                log.debug("Loco address {} finishes warrant {} and starts warrant {}",
996                        warrant.getSpeedUtil().getDccAddress(), _warrant.getDisplayName(), warrant.getDisplayName());
997            }
998            long time =  0;
999            for (int i = _idxCurrentCommand+1; i < _commands.size(); i++) {
1000                ThrottleSetting cmd = _commands.get(i);
1001                time += cmd.getTime();
1002            }
1003            // same address so this warrant (_warrant) must release the throttle before (warrant) can acquire it
1004            _checker = new CheckForTermination(_warrant, warrant, num, time);
1005            _checker.start();
1006            log.debug("Exit runWarrant");
1007            return;
1008        } else {
1009            java.awt.Color color = java.awt.Color.red;
1010            String msg = WarrantTableFrame.getDefault().runTrain(warrant, Warrant.MODE_RUN);
1011            if (msg == null) {
1012                msg = Bundle.getMessage("linkedLaunch",
1013                        warrant.getDisplayName(), _warrant.getDisplayName(),
1014                        warrant.getfirstOrder().getBlock().getDisplayName(),
1015                        _warrant.getfirstOrder().getBlock().getDisplayName());
1016                color = WarrantTableModel.myGreen;
1017            }
1018            if (Warrant._trace || log.isDebugEnabled()) {
1019                log.info("{} : Warrant Status", msg);
1020            }
1021            Engineer.setFrameStatusText(msg, color, true);
1022        }
1023    }
1024
1025    // send the messages on success of linked launch completion
1026    private void checkerDone(Warrant oldWarrant, Warrant newWarrant) {
1027        OBlock endBlock = oldWarrant.getLastOrder().getBlock();
1028        if (oldWarrant.getRunMode() != Warrant.MODE_NONE) {
1029            log.error("{} : Cannot Launch", Bundle.getMessage("cannotLaunch",
1030                    newWarrant.getDisplayName(), oldWarrant.getDisplayName(), endBlock.getDisplayName()));
1031            return;
1032        }
1033
1034        String msg = WarrantTableFrame.getDefault().runTrain(newWarrant, Warrant.MODE_RUN);
1035        java.awt.Color color = java.awt.Color.red;
1036        if (msg == null) {
1037            CommandValue cmdVal = _currentCommand.getValue();
1038            int num = Math.round(cmdVal.getFloat());
1039            if (oldWarrant.equals(newWarrant)) {
1040                msg = Bundle.getMessage("reLaunch", oldWarrant.getDisplayName(), (num<0 ? "unlimited" : num));
1041            } else {
1042                msg = Bundle.getMessage("linkedLaunch",
1043                        newWarrant.getDisplayName(), oldWarrant.getDisplayName(),
1044                        newWarrant.getfirstOrder().getBlock().getDisplayName(),
1045                        endBlock.getDisplayName());
1046            }
1047            color = WarrantTableModel.myGreen;
1048        }
1049        if (Warrant._trace || log.isDebugEnabled()) {
1050            log.info("{} : Launch", msg);
1051        }
1052        Engineer.setFrameStatusText(msg, color, true);
1053        _checker = null;
1054    }
1055
1056    private class CheckForTermination extends Thread {
1057        Warrant oldWarrant;
1058        Warrant newWarrant;
1059        long waitTime; // time to finish remaining commands
1060
1061        CheckForTermination(Warrant oldWar, Warrant newWar, int num, long limit) {
1062            oldWarrant = oldWar;
1063            newWarrant = newWar;
1064            waitTime = limit;
1065            if (log.isDebugEnabled()) log.debug("checkForTermination of \"{}\", before launching \"{}\". waitTime= {})",
1066                    oldWarrant.getDisplayName(), newWarrant.getDisplayName(), waitTime);
1067         }
1068
1069        @Override
1070        public void run() {
1071            long time = 0;
1072            synchronized (this) {
1073                while (time <= waitTime || oldWarrant.getRunMode() != Warrant.MODE_NONE) {
1074                    try {
1075                        wait(100);
1076                        time += 100;
1077                    } catch (InterruptedException ie) {
1078                        log.error("Engineer interrupted at CheckForTermination of \"{}\"", oldWarrant.getDisplayName(), ie);
1079                        _warrant.debugInfo();
1080                        Thread.currentThread().interrupt();
1081                        time = waitTime;
1082                    } finally {
1083                    }
1084                }
1085            }
1086            if (time > waitTime || log.isDebugEnabled()) {
1087                log.info("Waited {}ms for warrant \"{}\" to terminate. runMode={}",
1088                        time, oldWarrant.getDisplayName(), oldWarrant.getRunMode());
1089            }
1090            checkerDone(oldWarrant, newWarrant);
1091        }
1092    }
1093
1094    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY", justification="rampDone is called by ramp thread to clear Engineer waiting for it to finish")
1095    private void rampDone(boolean stop, String speedType, int endBlockIdx) {
1096        setIsRamping(false);
1097        if (!stop && !speedType.equals(Warrant.Stop)) {
1098            _speedType = speedType;
1099            setSpeedRatio(speedType);
1100            setWaitforClear(false);
1101            setHalt(false);
1102        }
1103        _stopPending = false;
1104        if (!_waitForClear && !_atHalt && !_atClear && !_holdRamp) {
1105            synchronized (this) {
1106                notifyAll();
1107            }
1108            log.debug("{}: rampDone called notify.", _warrant.getDisplayName());
1109            if (_currentCommand != null && _currentCommand.getCommand().equals(Command.NOOP)) {
1110                _idxCurrentCommand--;   // notify advances command.  Repeat wait for entry to next block
1111            }
1112        }
1113        if (log.isDebugEnabled()) {
1114            log.debug("{}: ThrottleRamp {} for speedType \"{}\". Thread.State= {}}", _warrant.getDisplayName(),
1115                    (stop?"stopped":"completed"), speedType, (_ramp != null?_ramp.getState():"_ramp is null!"));
1116        }
1117    }
1118
1119    /*
1120     * *************************************************************************************
1121     */
1122
1123     class ThrottleRamp extends Thread {
1124
1125         private String _endSpeedType;
1126         private int _endBlockIdx = -1;     // index of block where down ramp ends.
1127         private boolean stop = false;      // aborts ramping
1128         private boolean _rampDown = true;
1129         private boolean _die = false;      // kills ramp for good
1130         RampData rampData;
1131
1132         ThrottleRamp() {
1133            setName("Ramp(" + _warrant.getTrainName() +")");
1134            _endBlockIdx = -1;
1135         }
1136
1137         @SuppressFBWarnings(value = "NN_NAKED_NOTIFY", justification="quit is called by another thread to clear all ramp waits")
1138         void quit(boolean die) {
1139             stop = true;
1140             synchronized (this) {
1141                 notifyAll();    // free waits at the ramping time intervals
1142             }
1143             if (die) { // once set to true, do not allow resetting to false
1144                 _die = die;    // permanent shutdown, warrant running ending
1145                 synchronized (_rampLockObject) {
1146                     _rampLockObject.notifyAll(); // free wait at ramp run
1147                 }
1148             }
1149             log.debug("{}: ThrottleRamp clears _ramp waits", _warrant.getDisplayName());
1150         }
1151
1152        void setParameters(String endSpeedType, int endBlockIdx) {
1153            _endSpeedType = endSpeedType;
1154            _endBlockIdx = endBlockIdx;
1155            _stopPending = endSpeedType.equals(Warrant.Stop);
1156        }
1157
1158        boolean duplicate(String endSpeedType, int endBlockIdx) {
1159            if (endBlockIdx != _endBlockIdx ||
1160                    !endSpeedType.equals(_endSpeedType)) {
1161                return false;
1162            }
1163            return true;
1164        }
1165
1166        int getEndBlockIndex() {
1167            return _endBlockIdx;
1168        }
1169
1170        /**
1171         * @param blockIdx  index of block order where ramp finishes
1172         * @param cmdIdx   current command index
1173         * @return command index of block where commands should not be executed
1174         */
1175        int getCommandIndexLimit(int blockIdx, int cmdIdx) {
1176            // get next block
1177            int limit = _commands.size();
1178            String curBlkName = _warrant.getCurrentBlockName();
1179            String endBlkName = _warrant.getBlockAt(blockIdx).getDisplayName();
1180            if (!curBlkName.contentEquals(endBlkName)) {
1181                for (int cmd = cmdIdx; cmd < _commands.size(); cmd++) {
1182                    ThrottleSetting ts = _commands.get(cmd);
1183                    if (ts.getBeanDisplayName().equals(endBlkName) ) {
1184                        cmdIdx = cmd;
1185                        break;
1186                    }
1187                }
1188            }
1189            endBlkName = _warrant.getBlockAt(blockIdx+1).getDisplayName();
1190            for (int cmd = cmdIdx; cmd < _commands.size(); cmd++) {
1191                ThrottleSetting ts = _commands.get(cmd);
1192                if (ts.getBeanDisplayName().equals(endBlkName) &&
1193                        ts.getValue().getType().equals(ValueType.VAL_NOOP)) {
1194                    limit = cmd;
1195                    break;
1196                }
1197            }
1198            log.debug("getCommandIndexLimit: in end block {}, limitIdx = {} in block {}",
1199                    curBlkName, limit+1, _warrant.getBlockAt(blockIdx).getDisplayName());
1200            return limit;
1201        }
1202
1203        @Override
1204        @SuppressFBWarnings(value="UW_UNCOND_WAIT", justification="waits may be indefinite until satisfied or thread aborted")
1205        public void run() {
1206            while (!_die) {
1207                setIsRamping(false);
1208                synchronized (_rampLockObject) {
1209                    try {
1210                        _rampLockObject.wait(); // wait until notified by rampSpeedTo() calls quit()
1211                        setIsRamping(true);
1212                    } catch (InterruptedException ie) {
1213                        log.debug("As expected", ie);
1214                    }
1215                }
1216                if (_die) {
1217                    break;
1218                }
1219                stop = false;
1220                doRamp();
1221            }
1222        }
1223
1224//        @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "DLS_DEAD_LOCAL_STORE", justification = "Engineer needs _normalSpeed to be updated")
1225        @SuppressFBWarnings(value="SLF4J_FORMAT_SHOULD_BE_CONST", justification="False assumption")
1226        public void doRamp() {
1227            // At the the time 'right now' is the command indexed by _idxCurrentCommand-1"
1228            // is done. The main thread (Engineer) is waiting to do the _idxCurrentCommand.
1229            // A non-scripted speed change is to begin now.
1230            // If moving, the current speed is _normalSpeed modified by the current _speedType,
1231            // that is, the actual throttle setting.
1232            // If _endBlockIdx >= 0, this indexes the block where the the speed change must be
1233            // completed. the final speed change should occur just before entry into the next
1234            // block. This final speed change must be the exit speed of block '_endBlockIdx'
1235            // modified by _endSpeedType.
1236            // If _endBlockIdx < 0, for down ramps this should be a user initiated stop (Halt)
1237            // the endSpeed should be 0.
1238            // For up ramps, the _endBlockIdx and endSpeed are unknown. The script may have
1239            // speed changes scheduled during the time needed to up ramp. Note the code below
1240            // to negotiate and modify the RampData so that the end speed of the ramp makes a
1241            // smooth transition to the speed of the script (modified by _endSpeedType)
1242            // when the script resumes.
1243            // Non-speed commands are executed at their proper times during ramps.
1244            // Ramp calculations are based on the fact that the distance traveled during the
1245            // ramp is the same as the distance the unmodified script would travel, albeit
1246            // the times of travel are quite different.
1247            // Note on ramp up endSpeed should match scripted speed modified by endSpeedType
1248            float speed = getSpeedSetting();  // current speed setting
1249            float endSpeed;   // requested end speed
1250            int commandIndexLimit;
1251            if (_endBlockIdx >= 0) {
1252                commandIndexLimit = getCommandIndexLimit(_endBlockIdx, _idxCurrentCommand);
1253                endSpeed = _speedUtil.getBlockSpeedInfo(_endBlockIdx).getExitSpeed();
1254                endSpeed = _speedUtil.modifySpeed(endSpeed, _endSpeedType);
1255            } else {
1256                commandIndexLimit = _commands.size();
1257                endSpeed = _speedUtil.modifySpeed(_normalSpeed, _endSpeedType);
1258            }
1259            CommandValue cmdVal = _currentCommand.getValue();
1260            long timeToSpeedCmd = getTimeToNextCommand();
1261            _rampDown = endSpeed <= speed;
1262
1263            if (log.isDebugEnabled()) {
1264                log.debug("RAMP {} \"{}\" speed from {}, to {}, at block \"{}\" at Cmd#{} to Cmd#{}. timeToNextCmd= {}",
1265                       (_rampDown ? "DOWN" : "UP"), _endSpeedType, speed, endSpeed,
1266                       (_endBlockIdx>=0?_warrant.getBlockAt(_endBlockIdx).getDisplayName():"ahead"),
1267                       _idxCurrentCommand+1, commandIndexLimit, timeToSpeedCmd);
1268                       // Note: command indexes biased from 0 to 1 to match Warrant display of commands.
1269            }
1270            float scriptTrackSpeed = _speedUtil.getTrackSpeed(_normalSpeed);
1271
1272            int warBlockIdx = _warrant.getCurrentOrderIndex();  // block of current train position
1273            int cmdBlockIdx = -1;    // block of script commnd's train position
1274            int cmdIdx = _idxCurrentCommand;
1275            while (cmdIdx >= 0) {
1276                ThrottleSetting cmd  = _commands.get(--cmdIdx);
1277                if (cmd.getCommand().hasBlockName()) {
1278                    OBlock blk = (OBlock)cmd.getBean();
1279                    int idx = _warrant.getIndexOfBlockBefore(warBlockIdx, blk);
1280                    if (idx >= 0) {
1281                        cmdBlockIdx = idx;
1282                    } else {
1283                        cmdBlockIdx = _warrant.getIndexOfBlockAfter(blk, warBlockIdx);
1284                    }
1285                    break;
1286                }
1287            }
1288            if (cmdBlockIdx < 0) {
1289                cmdBlockIdx = warBlockIdx;
1290           }
1291
1292            synchronized (this) {
1293                try {
1294                    if (!_rampDown) {
1295                        // Up ramp may advance the train beyond the point where the script is interrupted.
1296                        // The ramp up will take time and the script may have other speed commands while
1297                        // ramping up. So the actual script speed may not match the endSpeed when the ramp
1298                        // up distance is traveled.  We must compare 'endSpeed' to 'scriptSpeed' at each
1299                        // step and skip scriptSpeeds to insure that endSpeed matches scriptSpeed when
1300                        // the ramp ends.
1301                        rampData = _speedUtil.getRampForSpeedChange(speed, 1.0f);
1302                        int timeIncrement = rampData.getRampTimeIncrement();
1303                        ListIterator<Float> iter = rampData.speedIterator(true);
1304                        speed = iter.next().floatValue();   // skip repeat of current speed
1305
1306                        float rampDist = 0;
1307                        float cmdDist = timeToSpeedCmd * scriptTrackSpeed;
1308
1309                        while (!stop && iter.hasNext()) {
1310                            speed = iter.next().floatValue();
1311                            float s = _speedUtil.modifySpeed(_normalSpeed, _endSpeedType);
1312                            if (speed > s) {
1313                                setSpeed(s);
1314                                break;
1315                            }
1316                            setSpeed(speed);
1317
1318                            // during ramp down the script may have non-speed commands that should be executed.
1319                            if (!stop && rampDist >= cmdDist && _idxCurrentCommand < commandIndexLimit) {
1320                                warBlockIdx = _warrant.getCurrentOrderIndex();  // current train position
1321                                if (_currentCommand.getCommand().hasBlockName()) {
1322                                    int idx = _warrant.getIndexOfBlockBefore(warBlockIdx, (OBlock)_currentCommand.getBean());
1323                                    if (idx >= 0) {
1324                                        cmdBlockIdx = idx;
1325                                    }
1326                                }
1327                                if (cmdBlockIdx <= warBlockIdx) {
1328                                    Command cmd = _currentCommand.getCommand();
1329                                    if (cmd.equals(Command.SPEED)) {
1330                                        cmdVal = _currentCommand.getValue();
1331                                        _normalSpeed = cmdVal.getFloat();
1332                                        scriptTrackSpeed = _speedUtil.getTrackSpeed(_normalSpeed);
1333                                        if (log.isDebugEnabled()) {
1334                                            log.debug("Cmd #{} for speed= {} skipped.",
1335                                                    _idxCurrentCommand+1, _normalSpeed);
1336                                        }
1337                                        cmdDist = 0;
1338                                    } else {
1339                                        executeComand(_currentCommand, timeIncrement);
1340                                    }
1341                                    if (_idxCurrentCommand < _commands.size() - 1) {
1342                                        _currentCommand = _commands.get(++_idxCurrentCommand);
1343                                        cmdDist = scriptTrackSpeed * _currentCommand.getTime();
1344                                    } else {
1345                                        cmdDist = 0;
1346                                    }
1347                                    rampDist = 0;
1348                                    advanceToCommandIndex(_idxCurrentCommand); // skip up to this command
1349                                }   // else Do not advance script commands of block ahead of train position
1350                            }
1351
1352                            try {
1353                                wait(timeIncrement);
1354                            } catch (InterruptedException ie) {
1355                                stop = true;
1356                            }
1357
1358                            rampDist += _speedUtil.getDistanceTraveled(speed, _speedType, timeIncrement);
1359                       }
1360
1361                    } else {     // decreasing, ramp down to a modified speed
1362                        // Down ramp may advance the train beyond the point where the script is interrupted.
1363                        // Any down ramp requested with _endBlockIdx >= 0 is expected to end at the end of
1364                        // a block i.e. the block of BlockOrder indexed by _endBlockIdx.
1365                        // Therefore script should resume at the exit to this block.
1366                        // During ramp down the script may have other Non speed commands that should be executed.
1367                        _warrant.downRampBegun(_endBlockIdx);
1368
1369                        rampData = _speedUtil.getRampForSpeedChange(speed, endSpeed);
1370                        int timeIncrement = rampData.getRampTimeIncrement();
1371                        ListIterator<Float> iter = rampData.speedIterator(false);
1372                        speed = iter.previous().floatValue();   // skip repeat of current throttle setting
1373
1374                        float rampDist = 0;
1375                        float cmdDist = timeToSpeedCmd * scriptTrackSpeed;
1376
1377                        while (!stop && iter.hasPrevious()) {
1378                            speed = iter.previous().floatValue();
1379                            setSpeed(speed);
1380
1381                            if (_endBlockIdx >= 0) {    // correction code for ramps that are too long or too short
1382                                int curIdx = _warrant.getCurrentOrderIndex();
1383                                if (curIdx > _endBlockIdx) {
1384                                    // loco overran end block.  Set end speed and leave ramp
1385                                    setSpeed(endSpeed);
1386                                    stop = true;
1387                                    log.warn("\"{}\" Ramp to speed \"{}\" ended due to overrun into block \"{}\". throttle {} set to {}.\"{}\"",
1388                                            _warrant.getTrainName(), _endSpeedType, _warrant.getBlockAt(curIdx).getDisplayName(),
1389                                            speed, endSpeed, _warrant.getDisplayName());
1390                                } else if ( curIdx < _endBlockIdx &&
1391                                        _endSpeedType.equals(Warrant.Stop) && Math.abs(speed - endSpeed) <.001f) {
1392                                    // At last speed change to set throttle was endSpeed, but train has not
1393                                    // reached the last block. Let loco creep to end block at current setting.
1394                                    if (log.isDebugEnabled())
1395                                        log.debug("Extending ramp to reach block {}. speed= {}",
1396                                                _warrant.getBlockAt(_endBlockIdx).getDisplayName(), speed);
1397                                    int waittime = 0;
1398                                    float throttleIncrement = _speedUtil.getRampThrottleIncrement();
1399                                    while (_endBlockIdx > _warrant.getCurrentOrderIndex() && waittime <= 60*timeIncrement && getSpeedSetting() > 0) {
1400                                        // Until loco reaches end block, continue current speed.
1401                                        if (waittime == 5*timeIncrement || waittime == 10*timeIncrement ||
1402                                                waittime == 15*timeIncrement || waittime == 20*timeIncrement) {
1403                                            // maybe train stalled on previous speed step. Bump speed up a notch at 3s, another at 9
1404                                            setSpeed(getSpeedSetting() + throttleIncrement);
1405                                        }
1406                                        try {
1407                                            wait(timeIncrement);
1408                                            waittime += timeIncrement;
1409                                        } catch (InterruptedException ie) {
1410                                            stop = true;
1411                                        }
1412                                    }
1413                                    try {
1414                                        wait(timeIncrement);
1415                                    } catch (InterruptedException ie) {
1416                                        stop = true;
1417                                    }
1418                                }
1419                            }
1420
1421                            // during ramp down the script may have non-speed commands that should be executed.
1422                            if (!stop && rampDist >= cmdDist && _idxCurrentCommand < commandIndexLimit) {
1423                                warBlockIdx = _warrant.getCurrentOrderIndex();  // current train position
1424                                if (_currentCommand.getCommand().hasBlockName()) {
1425                                    int idx = _warrant.getIndexOfBlockBefore(warBlockIdx, (OBlock)_currentCommand.getBean());
1426                                    if (idx >= 0) {
1427                                        cmdBlockIdx = idx;
1428                                    }
1429                                }
1430                                if (cmdBlockIdx <= warBlockIdx) {
1431                                    Command cmd = _currentCommand.getCommand();
1432                                    if (cmd.equals(Command.SPEED)) {
1433                                        cmdVal = _currentCommand.getValue();
1434                                        _normalSpeed = cmdVal.getFloat();
1435                                        scriptTrackSpeed = _speedUtil.getTrackSpeed(_normalSpeed);
1436                                        if (log.isDebugEnabled()) {
1437                                            log.debug("Cmd #{} for speed= {} skipped.",
1438                                                    _idxCurrentCommand+1, _normalSpeed);
1439                                        }
1440                                        cmdDist = 0;
1441                                    } else {
1442                                        executeComand(_currentCommand, timeIncrement);
1443                                    }
1444                                    if (_idxCurrentCommand < _commands.size() - 1) {
1445                                        _currentCommand = _commands.get(++_idxCurrentCommand);
1446                                        cmdDist = scriptTrackSpeed * _currentCommand.getTime();
1447                                    } else {
1448                                        cmdDist = 0;
1449                                    }
1450                                    rampDist = 0;
1451                                    advanceToCommandIndex(_idxCurrentCommand); // skip up to this command
1452                                }   // else Do not advance script commands of block ahead of train position
1453                            }
1454
1455                            try {
1456                                wait(timeIncrement);
1457                            } catch (InterruptedException ie) {
1458                                stop = true;
1459                            }
1460
1461                            rampDist += _speedUtil.getDistanceTraveled(speed, _speedType, timeIncrement);   // _speedType or Warrant.Normal??
1462                            //rampDist += getTrackSpeed(speed) * timeIncrement;
1463                       }
1464
1465                        // Ramp done, still in endBlock. Execute any remaining non-speed commands.
1466                       if (_endBlockIdx >= 0 && commandIndexLimit < _commands.size()) {
1467                            long cmdStart = System.currentTimeMillis();
1468                            while (_idxCurrentCommand < commandIndexLimit) {
1469                                NamedBean bean = _currentCommand.getBean();
1470                                if (bean instanceof OBlock) {
1471                                    if (_endBlockIdx < _warrant.getIndexOfBlockAfter((OBlock)bean, _endBlockIdx)) {
1472                                        // script is past end point, command should be NOOP.
1473                                        // regardless, don't execute any more commands.
1474                                        break;
1475                                    }
1476                                }
1477                                Command cmd = _currentCommand.getCommand();
1478                                if (cmd.equals(Command.SPEED)) {
1479                                    cmdVal = _currentCommand.getValue();
1480                                    _normalSpeed = cmdVal.getFloat();
1481                                    if (log.isDebugEnabled()) {
1482                                        log.debug("Cmd #{} for speed {} skipped. warrant {}",
1483                                                _idxCurrentCommand+1, _normalSpeed, _warrant.getDisplayName());
1484                                    }
1485                                } else {
1486                                    executeComand(_currentCommand, System.currentTimeMillis() - cmdStart);
1487                                }
1488                                _currentCommand = _commands.get(++_idxCurrentCommand);
1489                                advanceToCommandIndex(_idxCurrentCommand); // skip up to this command
1490                            }
1491                        }
1492                    }
1493
1494                } finally {
1495                    if (log.isDebugEnabled()) {
1496                        log.debug("Ramp Done. End Blk= {}, _idxCurrentCommand={} resumeIdx={}, commandIndexLimit={}. warrant {}",
1497                                (_endBlockIdx>=0?_warrant.getBlockAt(_endBlockIdx).getDisplayName():"not required"),
1498                                _idxCurrentCommand+1, _idxSkipToSpeedCommand, commandIndexLimit, _warrant.getDisplayName());
1499                    }
1500                }
1501            }
1502            rampDone(stop, _endSpeedType, _endBlockIdx);
1503            if (!stop) {
1504                _warrant.fireRunStatus("RampDone", _halt, _endSpeedType);   // normal completion of ramp
1505                if (Warrant._trace || log.isDebugEnabled()) {
1506                    log.info(Bundle.getMessage("RampSpeed", _warrant.getTrainName(),
1507                        _endSpeedType, _warrant.getCurrentBlockName()));
1508                }
1509            } else {
1510                if (Warrant._trace || log.isDebugEnabled()) {
1511                    log.info(Bundle.getMessage("RampSpeed", _warrant.getTrainName(),
1512                            _endSpeedType, _warrant.getCurrentBlockName()) + "-Interrupted!");
1513                }
1514
1515            }
1516            stop = false;
1517
1518            if (_rampDown) {    // check for overrun status last
1519                _warrant.downRampDone(stop, _halt, _endSpeedType, _endBlockIdx);
1520            }
1521        }
1522    }
1523
1524    private static final Logger log = LoggerFactory.getLogger(Engineer.class);
1525}