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