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