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