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