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