001package jmri.implementation;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.File;
006import java.util.ArrayList;
007import java.util.List;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011
012import jmri.InstanceManager;
013import jmri.JmriException;
014import jmri.NamedBean;
015import jmri.NamedBeanHandle;
016import jmri.NamedBeanUsageReport;
017import jmri.Route;
018import jmri.Sensor;
019import jmri.Turnout;
020import jmri.jmrit.Sound;
021import jmri.script.JmriScriptEngineManager;
022import jmri.util.ThreadingUtil;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * Class providing the basic logic of the Route interface.
029 *
030 * @see jmri.Route Route
031 * @author Dave Duchamp Copyright (C) 2004
032 * @author Bob Jacobsen Copyright (C) 2006, 2007
033 * @author Simon Reader Copyright (C) 2008
034 * @author Pete Cressman Copyright (C) 2009
035 */
036public class DefaultRoute extends AbstractNamedBean implements Route, java.beans.VetoableChangeListener {
037
038    /**
039     * Constructor for a Route instance with a given userName.
040     *
041     * @param systemName suggested system name
042     * @param userName   provided user name
043     */
044    public DefaultRoute(String systemName, String userName) {
045        super(systemName, userName);
046    }
047
048    /**
049     * Constructor for a Route instance.
050     *
051     * @param systemName suggested system name
052     */
053    public DefaultRoute(String systemName) {
054        super(systemName);
055        log.debug("default Route {} created", systemName);
056    }
057
058    /** {@inheritDoc} */
059    @Override
060    public String getBeanType() {
061        return Bundle.getMessage("BeanNameRoute");
062    }
063
064    /**
065     * Persistant instance variables (saved between runs).
066     */
067    private String mControlTurnout = "";
068    private NamedBeanHandle<Turnout> mControlNamedTurnout = null;
069    private int mControlTurnoutState = jmri.Turnout.THROWN;
070    private int mDelay = 0;
071    private boolean mTurnoutFeedbackIsCommanded = false;
072
073    private String mLockControlTurnout = "";
074    private NamedBeanHandle<Turnout> mLockControlNamedTurnout = null;
075    private int mLockControlTurnoutState = Turnout.THROWN;
076
077    private String mTurnoutsAlignedSensor = "";
078    private NamedBeanHandle<Sensor> mTurnoutsAlignedNamedSensor = null;
079
080    private String soundFilename;
081    private String scriptFilename;
082
083    private final jmri.NamedBeanHandleManager nbhm = InstanceManager.getDefault(jmri.NamedBeanHandleManager.class);
084
085    /**
086     * Operational instance variables (not saved between runs).
087     */
088    private ArrayList<OutputSensor> _outputSensorList = new ArrayList<>();
089
090    private class OutputSensor {
091
092        NamedBeanHandle<Sensor> _sensor;
093        int _state = Sensor.ACTIVE;
094
095        OutputSensor(String name) throws IllegalArgumentException {
096            Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(name);
097            _sensor = nbhm.getNamedBeanHandle(name, sensor);
098        }
099
100        String getName() {
101            if (_sensor != null) {
102                return _sensor.getName();
103            }
104            return null;
105        }
106
107        boolean setState(int state) {
108            if (_sensor == null) {
109                return false;
110            }
111            if ((state != Sensor.ACTIVE) && (state != Sensor.INACTIVE) && (state != Route.TOGGLE)) {
112                log.warn("Illegal Sensor state for Route: {}", getName());
113                return false;
114            }
115            _state = state;
116            return true;
117        }
118
119        int getState() {
120            return _state;
121        }
122
123        Sensor getSensor() {
124            if (_sensor != null) {
125                return _sensor.getBean();
126            }
127            return null;
128        }
129    }
130
131    private ArrayList<ControlSensor> _controlSensorList = new ArrayList<>();
132
133    private class ControlSensor extends OutputSensor implements PropertyChangeListener {
134
135        ControlSensor(String name) {
136            super(name);
137        }
138
139        @Override
140        boolean setState(int state) {
141            if (_sensor == null) {
142                return false;
143            }
144            _state = state;
145            return true;
146        }
147
148        void addListener() {
149            if (_sensor != null) {
150                _sensor.getBean().addPropertyChangeListener(this, getName(),
151                    "Route " + getDisplayName() + "Output Sensor");
152            }
153        }
154
155        void removeListener() {
156            if (_sensor != null) {
157                _sensor.getBean().removePropertyChangeListener(this);
158            }
159        }
160
161        @Override
162        public void propertyChange(PropertyChangeEvent e) {
163            if ( Sensor.PROPERTY_KNOWN_STATE.equals(e.getPropertyName())) {
164                int now = ((Integer) e.getNewValue());
165                int then = ((Integer) e.getOldValue());
166                checkSensor(now, then, (Sensor) e.getSource());
167            }
168        }
169    }
170
171    private transient PropertyChangeListener mTurnoutListener = null;
172    private transient PropertyChangeListener mLockTurnoutListener = null;
173
174    private ArrayList<OutputTurnout> _outputTurnoutList = new ArrayList<>();
175
176    private class OutputTurnout implements PropertyChangeListener {
177
178        NamedBeanHandle<Turnout> _turnout;
179        int _state;
180
181        OutputTurnout(String name) throws IllegalArgumentException {
182            Turnout turnout = InstanceManager.turnoutManagerInstance().provideTurnout(name);
183            _turnout = nbhm.getNamedBeanHandle(name, turnout);
184
185        }
186
187        String getName() {
188            if (_turnout != null) {
189                return _turnout.getName();
190            }
191            return null;
192        }
193
194        boolean setState(int state) {
195            if (_turnout == null) {
196                return false;
197            }
198            if ((state != Turnout.THROWN) && (state != Turnout.CLOSED) && (state != Route.TOGGLE)) {
199                log.warn("Illegal Turnout state for Route: {}", getName());
200                return false;
201            }
202            _state = state;
203            return true;
204        }
205
206        int getState() {
207            return _state;
208        }
209
210        Turnout getTurnout() {
211            return (_turnout != null ? _turnout.getBean() : null);
212        }
213
214        void addListener() {
215            if (_turnout != null) {
216                _turnout.getBean().addPropertyChangeListener(this, getName(),
217                    "Route " + getDisplayName() + " Output Turnout");
218            }
219        }
220
221        void removeListener() {
222            if (_turnout != null) {
223                _turnout.getBean().removePropertyChangeListener(this);
224            }
225        }
226
227        @Override
228        public void propertyChange(PropertyChangeEvent e) {
229            if ( Turnout.PROPERTY_KNOWN_STATE.equals(e.getPropertyName())
230                    || Turnout.PROPERTY_COMMANDED_STATE.equals(e.getPropertyName())) {
231                //check alignement of all turnouts in route
232                checkTurnoutAlignment();
233            }
234        }
235    }
236    private boolean busy = false;
237    private boolean _enabled = true;
238
239    /** {@inheritDoc} */
240    @Override
241    public boolean getEnabled() {
242        return _enabled;
243    }
244
245    /** {@inheritDoc} */
246    @Override
247    public void setEnabled(boolean v) {
248        boolean old = _enabled;
249        _enabled = v;
250        if (old != v) {
251            firePropertyChange(PROPERTY_ENABLED, old, v);
252        }
253    }
254
255    private boolean _locked = false;
256
257    /** {@inheritDoc} */
258    @Override
259    public boolean getLocked() {
260        return _locked;
261    }
262
263    /** {@inheritDoc} */
264    @Override
265    public void setLocked(boolean v) {
266        lockTurnouts(v);
267        boolean old = _locked;
268        _locked = v;
269        if (old != v) {
270            firePropertyChange(PROPERTY_ROUTE_LOCKED, old, v);
271        }
272    }
273
274    /** {@inheritDoc} */
275    @Override
276    public boolean canLock() {
277        for ( OutputTurnout oto : _outputTurnoutList){
278            Turnout to = oto.getTurnout();
279            if ( to !=null && to.canLock(Turnout.CABLOCKOUT)) {
280                return true;
281            }
282        }
283        return false;
284    }
285
286    /** {@inheritDoc} */
287    @Override
288    public boolean addOutputTurnout(String turnoutName, int turnoutState) {
289        OutputTurnout outputTurnout = new OutputTurnout(turnoutName);
290        if (!outputTurnout.setState(turnoutState)) {
291            return false;
292        }
293        _outputTurnoutList.add(outputTurnout);
294        return true;
295    }
296
297    /** {@inheritDoc} */
298    @Override
299    public void clearOutputTurnouts() {
300        _outputTurnoutList = new ArrayList<>();
301    }
302
303    /** {@inheritDoc} */
304    @Override
305    public int getNumOutputTurnouts() {
306        return _outputTurnoutList.size();
307    }
308
309    /** {@inheritDoc} */
310    @Override
311    @CheckForNull
312    public String getOutputTurnoutByIndex(int index) {
313        try {
314            return _outputTurnoutList.get(index).getName();
315        } catch (IndexOutOfBoundsException ioob) {
316            return null;
317        }
318    }
319
320    /** {@inheritDoc} */
321    @Override
322    public boolean isOutputTurnoutIncluded(String turnoutName) throws IllegalArgumentException {
323        Turnout t1 = InstanceManager.turnoutManagerInstance().provideTurnout(turnoutName);
324        return isOutputTurnoutIncluded(t1);
325    }
326
327    private boolean isOutputTurnoutIncluded(Turnout t1) {
328        for (int i = 0; i < _outputTurnoutList.size(); i++) {
329            if (_outputTurnoutList.get(i).getTurnout() == t1) {
330                // Found turnout
331                return true;
332            }
333        }
334        return false;
335    }
336
337    private void deleteOutputTurnout(Turnout t) {
338        int index = -1;
339        for (int i = 0; i < _outputTurnoutList.size(); i++) {
340            if (_outputTurnoutList.get(i).getTurnout() == t) {
341                index = i;
342                break;
343            }
344        }
345        if (index != -1) {
346            _outputTurnoutList.remove(index);
347        }
348    }
349
350    /** {@inheritDoc} */
351    @Override
352    public int getOutputTurnoutSetState(String name) throws IllegalArgumentException {
353        Turnout t1 = InstanceManager.turnoutManagerInstance().provideTurnout(name);
354        for (int i = 0; i < _outputTurnoutList.size(); i++) {
355            if (_outputTurnoutList.get(i).getTurnout() == t1) {
356                // Found turnout
357                return _outputTurnoutList.get(i).getState();
358            }
359        }
360        return -1;
361    }
362
363    /** {@inheritDoc} */
364    @Override
365    @CheckForNull
366    public Turnout getOutputTurnout(int k) {
367        try {
368            return _outputTurnoutList.get(k).getTurnout();
369        } catch (IndexOutOfBoundsException ioob) {
370            return null;
371        }
372    }
373
374    /** {@inheritDoc} */
375    @Override
376    public int getOutputTurnoutState(int k) {
377        try {
378            return _outputTurnoutList.get(k).getState();
379        } catch (IndexOutOfBoundsException ioob) {
380            return -1;
381        }
382    }
383
384    /** {@inheritDoc} */
385    @Override
386    public boolean addOutputSensor(String sensorName, int state) {
387        OutputSensor outputSensor = new OutputSensor(sensorName);
388        if (!outputSensor.setState(state)) {
389            return false;
390        }
391        _outputSensorList.add(outputSensor);
392        return true;
393    }
394
395    /** {@inheritDoc} */
396    @Override
397    public void clearOutputSensors() {
398        _outputSensorList = new ArrayList<>();
399    }
400
401    @Override
402    public int getNumOutputSensors() {
403        return _outputSensorList.size();
404    }
405
406    /** {@inheritDoc} */
407    @Override
408    @CheckForNull
409    public String getOutputSensorByIndex(int index) {
410        try {
411            return _outputSensorList.get(index).getName();
412        } catch (IndexOutOfBoundsException ioob) {
413            return null;
414        }
415    }
416
417    /** {@inheritDoc} */
418    @Override
419    public boolean isOutputSensorIncluded(String sensorName) throws IllegalArgumentException {
420        Sensor s1 = InstanceManager.sensorManagerInstance().provideSensor(sensorName);
421        return isOutputSensorIncluded(s1);
422    }
423
424    private boolean isOutputSensorIncluded(Sensor s1) {
425        for (int i = 0; i < _outputSensorList.size(); i++) {
426            if (_outputSensorList.get(i).getSensor() == s1) {
427                // Found turnout
428                return true;
429            }
430        }
431        return false;
432    }
433
434    /** {@inheritDoc} */
435    @Override
436    public int getOutputSensorSetState(String name) throws IllegalArgumentException {
437        Sensor s1 = InstanceManager.sensorManagerInstance().provideSensor(name);
438        for (int i = 0; i < _outputSensorList.size(); i++) {
439            if (_outputSensorList.get(i).getSensor() == s1) {
440                // Found turnout
441                return _outputSensorList.get(i).getState();
442            }
443        }
444        return -1;
445    }
446
447    /** {@inheritDoc} */
448    @Override
449    @CheckForNull
450    public Sensor getOutputSensor(int k) {
451        try {
452            return _outputSensorList.get(k).getSensor();
453        } catch (IndexOutOfBoundsException ioob) {
454            return null;
455        }
456    }
457
458    /** {@inheritDoc} */
459    @Override
460    public int getOutputSensorState(int k) {
461        try {
462            return _outputSensorList.get(k).getState();
463        } catch (IndexOutOfBoundsException ioob) {
464            return -1;
465        }
466    }
467
468    private void removeOutputSensor(Sensor s) {
469        int index = -1;
470        for (int i = 0; i < _outputSensorList.size(); i++) {
471            if (_outputSensorList.get(i).getSensor() == s) {
472                index = i;
473                break;
474            }
475        }
476        if (index != -1) {
477            _outputSensorList.remove(index);
478        }
479    }
480
481    /** {@inheritDoc} */
482    @Override
483    public void setOutputScriptName(String filename) {
484        scriptFilename = filename;
485    }
486
487    /** {@inheritDoc} */
488    @Override
489    @CheckForNull
490    public String getOutputScriptName() {
491        return scriptFilename;
492    }
493
494    /** {@inheritDoc} */
495    @Override
496    public void setOutputSoundName(String filename) {
497        soundFilename = filename;
498    }
499
500    /** {@inheritDoc} */
501    @Override
502    @CheckForNull
503    public String getOutputSoundName() {
504        return soundFilename;
505    }
506
507    /** {@inheritDoc} */
508    @Override
509    public void setTurnoutsAlignedSensor(@CheckForNull String sensorName) throws IllegalArgumentException {
510        log.debug("setTurnoutsAlignedSensor {} {}", getSystemName(), sensorName);
511
512        mTurnoutsAlignedSensor = sensorName;
513        if (mTurnoutsAlignedSensor == null || mTurnoutsAlignedSensor.isEmpty()) {
514            mTurnoutsAlignedNamedSensor = null;
515            return;
516        }
517        Sensor s = InstanceManager.sensorManagerInstance().provideSensor(mTurnoutsAlignedSensor);
518        mTurnoutsAlignedNamedSensor = nbhm.getNamedBeanHandle(mTurnoutsAlignedSensor, s);
519    }
520
521    /** {@inheritDoc} */
522    @Override
523    @CheckForNull
524    public String getTurnoutsAlignedSensor() {
525        if (mTurnoutsAlignedNamedSensor != null) {
526            return mTurnoutsAlignedNamedSensor.getName();
527        }
528        return mTurnoutsAlignedSensor;
529    }
530
531    /** {@inheritDoc} */
532    @Override
533    @CheckForNull
534    public Sensor getTurnoutsAlgdSensor() throws IllegalArgumentException {
535        if (mTurnoutsAlignedNamedSensor != null) {
536            return mTurnoutsAlignedNamedSensor.getBean();
537        } else if (mTurnoutsAlignedSensor != null && !mTurnoutsAlignedSensor.isEmpty()) {
538            Sensor s = InstanceManager.sensorManagerInstance().provideSensor(mTurnoutsAlignedSensor);
539            mTurnoutsAlignedNamedSensor = nbhm.getNamedBeanHandle(mTurnoutsAlignedSensor, s);
540            return s;
541        }
542        return null;
543    }
544    // Inputs ----------------
545
546    /** {@inheritDoc} */
547    @Override
548    public void clearRouteSensors() {
549        _controlSensorList = new ArrayList<>();
550    }
551
552    /**
553     * Method returns true if the sensor provided is already in the list of
554     * control sensors for this route.
555     *
556     * @param sensor the sensor to check for
557     * @return true if the sensor is found, false otherwise
558     */
559    private boolean isControlSensorIncluded(@Nonnull ControlSensor sensor) {
560        for (int i = 0; i < _controlSensorList.size(); i++) {
561            if (_controlSensorList.get(i).getName().equals(sensor.getName())
562                    && _controlSensorList.get(i).getState() == sensor.getState()) {
563                return true;
564            }
565        }
566        return false;
567    }
568
569    /** {@inheritDoc} */
570    @Override
571    public boolean addSensorToRoute(String sensorName, int mode) {
572        log.debug("addSensorToRoute({}, {}) as {} in {}", sensorName, mode, _controlSensorList.size(), getSystemName());
573
574        ControlSensor sensor = new ControlSensor(sensorName);
575        if (!sensor.setState(mode)) {
576            return false;
577        }
578        if (isControlSensorIncluded(sensor)) {
579            // this is a normal condition, but log in case
580            log.debug("Not adding duplicate control sensor {} to route {}", sensorName, getSystemName());
581        } else {
582            _controlSensorList.add(sensor);
583        }
584
585        if (_controlSensorList.size() > MAX_CONTROL_SENSORS) {
586            // reached maximum
587            log.warn("Sensor {} exceeded maximum number of control Sensors for Route: {}", sensorName, getSystemName());
588        }
589
590        return true;
591    }
592
593    /** {@inheritDoc} */
594    @Override
595    @CheckForNull
596    public String getRouteSensorName(int index) {
597        try {
598            return _controlSensorList.get(index).getName();
599        } catch (IndexOutOfBoundsException ioob) {
600            return null;
601        }
602    }
603
604    /** {@inheritDoc} */
605    @Override
606    @CheckForNull
607    public Sensor getRouteSensor(int index) {
608        try {
609            return _controlSensorList.get(index).getSensor();
610        } catch (IndexOutOfBoundsException ioob) {
611            return null;
612        }
613    }
614
615    /** {@inheritDoc} */
616    @Override
617    public int getRouteSensorMode(int index) {
618        try {
619            return _controlSensorList.get(index).getState();
620        } catch (IndexOutOfBoundsException ioob) {
621            return 0;
622        }
623    }
624
625    boolean isRouteSensorIncluded(Sensor s) {
626        for (int i = 0; i < _controlSensorList.size(); i++) {
627            if (_controlSensorList.get(i).getSensor() == s) {
628                // Found turnout
629                return true;
630            }
631        }
632        return false;
633    }
634
635    void removeRouteSensor(Sensor s) {
636        int index = -1;
637        for (int i = 0; i < _controlSensorList.size(); i++) {
638            if (_controlSensorList.get(i).getSensor() == s) {
639                index = i;
640                break;
641            }
642        }
643        if (index != -1) {
644            _controlSensorList.remove(index);
645        }
646    }
647
648    /** {@inheritDoc} */
649    @Override
650    public void setControlTurnout(@CheckForNull String turnoutName) throws IllegalArgumentException {
651        mControlTurnout = turnoutName;
652        if (mControlTurnout == null || mControlTurnout.isEmpty()) {
653            mControlNamedTurnout = null;
654            return;
655        }
656        Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(mControlTurnout);
657        mControlNamedTurnout = nbhm.getNamedBeanHandle(mControlTurnout, t);
658    }
659
660    /** {@inheritDoc} */
661    @Override
662    @CheckForNull
663    public String getControlTurnout() {
664        if (mControlNamedTurnout != null) {
665            return mControlNamedTurnout.getName();
666        }
667        return mControlTurnout;
668    }
669
670    /** {@inheritDoc} */
671    @Override
672    @CheckForNull
673    public Turnout getCtlTurnout() throws IllegalArgumentException {
674        if (mControlNamedTurnout != null) {
675            return mControlNamedTurnout.getBean();
676        } else if (mControlTurnout != null && !mControlTurnout.isEmpty()) {
677            Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(mControlTurnout);
678            mControlNamedTurnout = nbhm.getNamedBeanHandle(mControlTurnout, t);
679            return t;
680        }
681        return null;
682    }
683
684    /** {@inheritDoc} */
685    @Override
686    public void setLockControlTurnout(@CheckForNull String turnoutName) throws IllegalArgumentException {
687        mLockControlTurnout = turnoutName;
688        if (mLockControlTurnout == null || mLockControlTurnout.isEmpty()) {
689            mLockControlNamedTurnout = null;
690            return;
691        }
692        Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(mLockControlTurnout);
693        mLockControlNamedTurnout = nbhm.getNamedBeanHandle(mLockControlTurnout, t);
694    }
695
696    /** {@inheritDoc} */
697    @Override
698    @CheckForNull
699    public String getLockControlTurnout() {
700        if (mLockControlNamedTurnout != null) {
701            return mLockControlNamedTurnout.getName();
702        }
703        return mLockControlTurnout;
704    }
705
706    /** {@inheritDoc} */
707    @Override
708    @CheckForNull
709    public Turnout getLockCtlTurnout() throws IllegalArgumentException {
710        if (mLockControlNamedTurnout != null) {
711            return mLockControlNamedTurnout.getBean();
712        } else if (mLockControlTurnout != null && !mLockControlTurnout.isEmpty()) {
713            Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(mLockControlTurnout);
714            mLockControlNamedTurnout = nbhm.getNamedBeanHandle(mLockControlTurnout, t);
715            return t;
716        }
717        return null;
718    }
719
720    /** {@inheritDoc} */
721    @Override
722    public void setRouteCommandDelay(int delay) {
723        if (delay >= 0) {
724            mDelay = delay;
725        }
726    }
727
728    /** {@inheritDoc} */
729    @Override
730    public int getRouteCommandDelay() {
731        return mDelay;
732    }
733
734    /** {@inheritDoc} */
735    @Override
736    public void setControlTurnoutState(int turnoutState) {
737        if ((turnoutState == Route.ONTHROWN)
738                || (turnoutState == Route.ONCLOSED)
739                || (turnoutState == Route.ONCHANGE)
740                || (turnoutState == Route.VETOCLOSED)
741                || (turnoutState == Route.VETOTHROWN)) {
742            mControlTurnoutState = turnoutState;
743        } else {
744            log.error("Attempt to set invalid control Turnout state for Route.");
745        }
746    }
747
748    /** {@inheritDoc} */
749    @Override
750    public int getControlTurnoutState() {
751        return mControlTurnoutState;
752    }
753
754    /** {@inheritDoc} */
755    @Override
756    public void setControlTurnoutFeedback(boolean turnoutFeedbackIsCommanded) {
757        mTurnoutFeedbackIsCommanded  = turnoutFeedbackIsCommanded;
758    }
759
760    /** {@inheritDoc} */
761    @Override
762    public boolean getControlTurnoutFeedback() {
763        return mTurnoutFeedbackIsCommanded;
764    }
765
766    /** {@inheritDoc} */
767    @Override
768    public void setLockControlTurnoutState(int turnoutState) {
769        if ((turnoutState == Route.ONTHROWN)
770                || (turnoutState == Route.ONCLOSED)
771                || (turnoutState == Route.ONCHANGE)) {
772            mLockControlTurnoutState = turnoutState;
773        } else {
774            log.error("Attempt to set invalid lock control Turnout state for Route.");
775        }
776    }
777
778    /** {@inheritDoc} */
779    @Override
780    public int getLockControlTurnoutState() {
781        return mLockControlTurnoutState;
782    }
783
784    /**
785     * Lock or unlock turnouts that are part of a route
786     */
787    private void lockTurnouts(boolean lock) {
788        // determine if turnout should be locked
789        for (int i = 0; i < _outputTurnoutList.size(); i++) {
790            _outputTurnoutList.get(i).getTurnout().setLocked(
791                Turnout.CABLOCKOUT + Turnout.PUSHBUTTONLOCKOUT, lock);
792        }
793    }
794
795    /** {@inheritDoc} */
796    @Override
797    public void setRoute() {
798        if ((!_outputTurnoutList.isEmpty())
799                || (!_outputSensorList.isEmpty())
800                || (soundFilename != null)
801                || (scriptFilename != null)) {
802            if (!busy) {
803                log.debug("Setting route {}", this.getSystemName());
804                setRouteBusy(true);
805                SetRouteThread thread = new SetRouteThread(this);
806                thread.setName("Route "+getDisplayName()+" setRoute");
807                thread.start();
808            } else {
809                log.debug("Not setting route {} because busy", this.getSystemName());
810            }
811        } else {
812            log.debug("Unable to set route {} because no turnouts or no sensors", this.getSystemName());
813        }
814    }
815
816    /**
817     * Handle sensor update event to see if it will set the route.
818     * <p>
819     * Called when a "KnownState" event is received, it assumes that only one
820     * sensor is changing right now, so can use state calls for everything other
821     * than this sensor.
822     * <p>
823     * This will fire the Route if the conditions are correct.
824     * <p>
825     * Returns nothing explicitly, but has the side effect of firing route.
826     *
827     * @param newState new state of control sensor
828     * @param oldState former state
829     * @param sensor   Sensor used as Route control sensor
830     */
831    protected void checkSensor(int newState, int oldState, @Nonnull Sensor sensor) {
832        // check for veto of change
833        if (isVetoed()) {
834            return; // don't fire
835        }
836        String name = sensor.getSystemName();
837        log.debug("check Sensor {} for {}", name, getSystemName());
838        boolean fire = false;  // dont fire unless we find something
839        for (int i = 0; i < _controlSensorList.size(); i++) {
840            Sensor s = getRouteSensor(i);
841            if (s !=null && s.equals(sensor)) {
842                // here for match, check mode & handle onActive, onInactive
843                int mode = getRouteSensorMode(i);
844                log.debug("match mode: {} new state: {} old state: {}", mode, newState, oldState);
845
846                // if in target mode, note whether to act
847                if (((mode == ONACTIVE) && (newState == Sensor.ACTIVE))
848                        || ((mode == ONINACTIVE) && (newState == Sensor.INACTIVE))
849                        || ((mode == ONCHANGE) && (newState != oldState))) {
850                    fire = true;
851                }
852
853                // if any other modes, just skip because
854                // the sensor might be in list more than once
855            }
856        }
857
858        log.debug("check activated");
859        if (!fire) {
860            return;
861        }
862
863        // and finally set the route
864        log.debug("call setRoute for {}", getSystemName());
865        setRoute();
866    }
867
868    /**
869     * Turnout has changed, check to see if this fires.
870     * <p>
871     * Will fire Route if appropriate.
872     *
873     * @param newState new state of control turnout
874     * @param oldState former state
875     * @param t        Turnout used as Route control turnout
876     */
877    void checkTurnout(int newState, int oldState, Turnout t) {
878        if (isVetoed()) {
879            return; // skip setting route
880        }
881        switch (mControlTurnoutState) {
882            case ONCLOSED:
883                if (newState == Turnout.CLOSED) {
884                    setRoute();
885                }
886                break;
887            case ONTHROWN:
888                if (newState == Turnout.THROWN) {
889                    setRoute();
890                }
891                break;
892            case ONCHANGE:
893                if (newState != oldState) {
894                    setRoute();
895                }
896                break;
897            default:
898                break; // not a firing state
899        }
900    }
901
902    /**
903     * Turnout has changed, check to see if this will lock or unlock route.
904     *
905     * @param newState  new state of lock turnout
906     * @param oldState  former turnout state
907     * @param t         Turnout used for locking the Route
908     */
909    void checkLockTurnout(int newState, int oldState, Turnout t) {
910        switch (mLockControlTurnoutState) {
911            case ONCLOSED:
912                setLocked(newState == Turnout.CLOSED);
913                break;
914            case ONTHROWN:
915                setLocked(newState == Turnout.THROWN);
916                break;
917            case ONCHANGE:
918                if (newState != oldState) {
919                    setLocked(!getLocked());
920                }
921                break;
922            default: // if none, return
923        }
924    }
925
926    /**
927     * Method to check if the turnouts for this route are correctly aligned.
928     * Sets turnouts aligned sensor (if there is one) to active if the turnouts
929     * are aligned. Sets the sensor to inactive if they are not aligned
930     */
931    public void checkTurnoutAlignment() {
932
933        //check each of the output turnouts in turn
934        //turnouts are deemed not aligned if:
935        // - commanded and known states don't agree
936        // - non-toggle turnouts known state not equal to desired state
937        // turnouts aligned sensor is then set accordingly
938        Sensor sensor = this.getTurnoutsAlgdSensor();
939        if (sensor != null) {
940            try {
941                // this method can be called multiple times while a route is
942                // still going ACTIVE, so short-circut out as INCONSISTENT if
943                // isRouteBusy() is true; this ensures nothing watching the
944                // route shows it as ACTIVE when it may not really be
945                if (this.isRouteBusy()) {
946                    sensor.setKnownState(Sensor.INCONSISTENT);
947                    return;
948                }
949                for (OutputTurnout ot : this._outputTurnoutList) {
950                    Turnout turnout = ot.getTurnout();
951                    int targetState = ot.getState();
952                    if (!turnout.isConsistentState()) {
953                        sensor.setKnownState(Sensor.INCONSISTENT);
954                        return;
955                    }
956                    if (targetState != Route.TOGGLE && targetState != turnout.getKnownState()) {
957                        sensor.setKnownState(Sensor.INACTIVE);
958                        return;
959                    }
960                }
961                sensor.setKnownState(Sensor.ACTIVE);
962            } catch (JmriException ex) {
963                log.warn("Exception setting sensor {} in route", getTurnoutsAlignedSensor());
964            }
965        }
966    }
967
968    /** {@inheritDoc} */
969    @Override
970    public void activateRoute() {
971        activatedRoute = true;
972
973        String tas = getTurnoutsAlignedSensor();
974        //register output turnouts to return Known State if a turnouts aligned sensor is defined
975        if ( tas != null && !tas.isEmpty()) {
976
977            for (int k = 0; k < _outputTurnoutList.size(); k++) {
978                _outputTurnoutList.get(k).addListener();
979            }
980        }
981
982        for (int k = 0; k < _controlSensorList.size(); k++) {
983            _controlSensorList.get(k).addListener();
984        }
985        Turnout ctl = getCtlTurnout();
986        if (ctl != null) {
987            mTurnoutListener = e -> {
988                String name = Turnout.PROPERTY_KNOWN_STATE;
989                if (this.getControlTurnoutFeedback()) {
990                    name = Turnout.PROPERTY_COMMANDED_STATE;
991                }
992                if (e.getPropertyName().equals(name)) {
993                    int now = ((Integer) e.getNewValue());
994                    int then = ((Integer) e.getOldValue());
995                    checkTurnout(now, then, (Turnout) e.getSource());
996                }
997            };
998            ctl.addPropertyChangeListener(mTurnoutListener, getControlTurnout(), "Route " + getDisplayName());
999        }
1000        Turnout lockCtl = getLockCtlTurnout();
1001        if (lockCtl != null) {
1002            mLockTurnoutListener = e -> {
1003                if ( Turnout.PROPERTY_KNOWN_STATE.equals(e.getPropertyName())) {
1004                    int now = ((Integer) e.getNewValue());
1005                    int then = ((Integer) e.getOldValue());
1006                    checkLockTurnout(now, then, (Turnout) e.getSource());
1007                }
1008            };
1009            lockCtl.addPropertyChangeListener(mLockTurnoutListener,
1010                getLockControlTurnout(), "Route " + getDisplayName());
1011        }
1012
1013        checkTurnoutAlignment();
1014        // register for updates to the Output Turnouts
1015    }
1016
1017    /**
1018     * Internal method to check whether operation of the route has been vetoed
1019     * by a sensor or turnout setting.
1020     *
1021     * @return true if veto, i.e. don't fire route; false if no veto, OK to fire
1022     */
1023    boolean isVetoed() {
1024        log.debug("check for veto");
1025        // check this route not enabled
1026        if (!_enabled) {
1027            return true;
1028        }
1029
1030        // check sensors
1031        for (int i = 0; i < _controlSensorList.size(); i++) {
1032            ControlSensor controlSensor = _controlSensorList.get(i);
1033            int s = controlSensor.getSensor().getKnownState();
1034            int mode = controlSensor.getState();
1035            if (((mode == VETOACTIVE) && (s == Sensor.ACTIVE))
1036                    || ((mode == VETOINACTIVE) && (s == Sensor.INACTIVE))) {
1037                return true;  // veto set
1038            }
1039        }
1040        // check control turnout
1041        Turnout ctl = getCtlTurnout();
1042        if (ctl != null) {
1043            int tstate = ctl.getKnownState();
1044            if (mControlTurnoutState == Route.VETOCLOSED && tstate == Turnout.CLOSED) {
1045                return true;
1046            }
1047            if (mControlTurnoutState == Route.VETOTHROWN && tstate == Turnout.THROWN) {
1048                return true;
1049            }
1050        }
1051        return false;
1052    }
1053
1054    /** {@inheritDoc} */
1055    @Override
1056    public void deActivateRoute() {
1057        //Check that the route isn't already deactived.
1058        if (!activatedRoute) {
1059            return;
1060        }
1061
1062        activatedRoute = false;
1063        // remove control turnout if there's one
1064        for (int k = 0; k < _controlSensorList.size(); k++) {
1065            _controlSensorList.get(k).removeListener();
1066        }
1067        if (mTurnoutListener != null) {
1068            Turnout ctl = getCtlTurnout();
1069            if (ctl != null) {
1070                ctl.removePropertyChangeListener(mTurnoutListener);
1071            }
1072            mTurnoutListener = null;
1073        }
1074        // remove lock control turnout if there's one
1075        if (mLockTurnoutListener != null) {
1076            Turnout lockCtl = getCtlTurnout();
1077            if (lockCtl != null) {
1078                lockCtl.removePropertyChangeListener(mLockTurnoutListener);
1079            }
1080            mLockTurnoutListener = null;
1081        }
1082        //remove listeners on output turnouts if there are any
1083        if (!mTurnoutsAlignedSensor.isEmpty()) {
1084            for (int k = 0; k < _outputTurnoutList.size(); k++) {
1085                _outputTurnoutList.get(k).removeListener();
1086            }
1087        }
1088    }
1089
1090    private boolean activatedRoute = false;
1091
1092    /**
1093     * Mark the Route as transitioning to an {@link jmri.Sensor#ACTIVE} state.
1094     *
1095     * @param busy true if Route should be busy.
1096     */
1097    protected void setRouteBusy(boolean busy) {
1098        this.busy = busy;
1099        this.checkTurnoutAlignment();
1100    }
1101
1102    /**
1103     * Method to query if Route is busy (returns true if commands are being
1104     * issued to Route turnouts)
1105     *
1106     * @return true if the Route is transitioning to an
1107     *         {@link jmri.Sensor#ACTIVE} state, false otherwise.
1108     */
1109    protected boolean isRouteBusy() {
1110        return busy;
1111    }
1112
1113    /** {@inheritDoc} */
1114    @Override
1115    public int getState() {
1116        Sensor s = getTurnoutsAlgdSensor();
1117        if (s != null) {
1118            return s.getKnownState();
1119        }
1120        return UNKNOWN;
1121    }
1122
1123    /** {@inheritDoc} */
1124    @Override
1125    public void setState(int state) {
1126        setRoute();
1127    }
1128
1129    /** {@inheritDoc} */
1130    @Override
1131    public void vetoableChange(PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
1132        NamedBean nb = (NamedBean) evt.getOldValue();
1133        if ( jmri.Manager.PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) {
1134            StringBuilder message = new StringBuilder();
1135            message.append("<b>").append(getDisplayName()).append("</b><ul>"); // NOI18N
1136            boolean found = false;
1137            if (nb instanceof Turnout) {
1138                if (isOutputTurnoutIncluded((Turnout) nb)) {
1139                    message.append(Bundle.getMessage("InUseRouteOutputTurnout")); // NOI18N
1140                    found = true;
1141                }
1142                if (nb.equals(getCtlTurnout())) {
1143                    message.append(Bundle.getMessage("InUseRouteControlTurnout")); // NOI18N
1144                    found = true;
1145                }
1146                if (nb.equals(getLockCtlTurnout())) {
1147                    message.append(Bundle.getMessage("InUseRouteLockTurnout")); // NOI18N
1148                    found = true;
1149                }
1150            } else if (nb instanceof Sensor) {
1151                if (isOutputSensorIncluded((Sensor) nb)) {
1152                    message.append(Bundle.getMessage("InUseRouteOutputSensor")); // NOI18N
1153                    found = true;
1154                }
1155                if (nb.equals(getTurnoutsAlgdSensor())) {
1156                    message.append(Bundle.getMessage("InUseRouteAlignSensor")); // NOI18N
1157                    found = true;
1158                }
1159                if (isRouteSensorIncluded((Sensor) nb)) {
1160                    message.append(Bundle.getMessage("InUseRouteSensor")); // NOI18N
1161                    found = true;
1162                }
1163
1164            }
1165            if (found) {
1166                message.append("</ul>");
1167                throw new java.beans.PropertyVetoException(message.toString(), evt);
1168            }
1169        } else if (jmri.Manager.PROPERTY_DO_DELETE.equals(evt.getPropertyName())) {
1170            if (nb instanceof Turnout) {
1171                if (isOutputTurnoutIncluded((Turnout) nb)) {
1172                    deActivateRoute();
1173                    deleteOutputTurnout((Turnout) evt.getOldValue());
1174                }
1175                if (nb.equals(getCtlTurnout())) {
1176                    deActivateRoute();
1177                    setControlTurnout(null);
1178                }
1179                if (nb.equals(getLockCtlTurnout())) {
1180                    deActivateRoute();
1181                    setLockControlTurnout(null);
1182                }
1183            } else if (nb instanceof Sensor) {
1184                if (isOutputSensorIncluded((Sensor) nb)) {
1185                    deActivateRoute();
1186                    removeOutputSensor((Sensor) nb);
1187                }
1188                if (nb.equals(getTurnoutsAlgdSensor())) {
1189                    deActivateRoute();
1190                    setTurnoutsAlignedSensor(null);
1191                }
1192                if (isRouteSensorIncluded((Sensor) nb)) {
1193                    deActivateRoute();
1194                    removeRouteSensor((Sensor) nb);
1195                }
1196            }
1197            activateRoute();
1198        }
1199    }
1200
1201    @Override
1202    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1203        List<NamedBeanUsageReport> report = new ArrayList<>();
1204        if (bean != null) {
1205            for (int i = 0; i < getNumOutputTurnouts(); i++) {
1206                if (bean.equals(getOutputTurnout(i))) {
1207                    report.add(new NamedBeanUsageReport("RouteTurnoutOutput"));  // NOI18N
1208                }
1209            }
1210            for (int i = 0; i < getNumOutputSensors(); i++) {
1211                if (bean.equals(getOutputSensor(i))) {
1212                    report.add(new NamedBeanUsageReport("RouteSensorOutput"));  // NOI18N
1213                }
1214            }
1215            for (int i = 0; i < _controlSensorList.size(); i++) {
1216                if (bean.equals(getRouteSensor(i))) {
1217                    report.add(new NamedBeanUsageReport("RouteSensorControl"));  // NOI18N
1218                }
1219            }
1220            if (bean.equals(getTurnoutsAlgdSensor())) {
1221                report.add(new NamedBeanUsageReport("RouteSensorAligned"));  // NOI18N
1222            }
1223            if (bean.equals(getCtlTurnout())) {
1224                report.add(new NamedBeanUsageReport("RouteTurnoutControl"));  // NOI18N
1225            }
1226            if (bean.equals(getLockCtlTurnout())) {
1227                report.add(new NamedBeanUsageReport("RouteTurnoutLock"));  // NOI18N
1228            }
1229        }
1230        return report;
1231    }
1232
1233    private static final Logger log = LoggerFactory.getLogger(DefaultRoute.class);
1234
1235    /**
1236     * Class providing a thread to set route turnouts.
1237     */
1238    private static class SetRouteThread extends Thread {
1239
1240        /**
1241         * Constructs the thread.
1242         *
1243         * @param aRoute DefaultRoute to set
1244         */
1245        private SetRouteThread(DefaultRoute aRoute) {
1246            r = aRoute;
1247        }
1248
1249        /**
1250         * Runs the thread - performs operations in the order:
1251         * <ul>
1252         * <li>Run script (can run in parallel)
1253         * <li>Play Sound (runs in parallel)
1254         * <li>Set Sensors
1255         * <li>Set Turnouts
1256         * </ul>
1257         */
1258        @Override
1259        public void run() {
1260            runScript();
1261            playSound();
1262            setSensors();
1263            setTurnouts();
1264            // set route not busy
1265            r.setRouteBusy(false);
1266        }
1267
1268        private void runScript() {
1269            // run script defined for start of route set
1270            String scriptName = r.getOutputScriptName();
1271            if ((scriptName != null) && (!scriptName.isEmpty())) {
1272                JmriScriptEngineManager.getDefault().runScript(
1273                    new File(jmri.util.FileUtil.getExternalFilename(scriptName)));
1274            }
1275        }
1276
1277        private void playSound() {
1278            // play sound defined for start of route set
1279            var outputSoundName = r.getOutputSoundName();
1280            if (( outputSoundName != null) && (!outputSoundName.isEmpty())) {
1281                try {
1282                    (new Sound(outputSoundName)).play(true);
1283                } catch (NullPointerException ex) {
1284                    setRouteLog.error("Cannot find Sound file {} for Route {}", outputSoundName, r.getDisplayName());
1285                }
1286            }
1287        }
1288
1289        private void setSensors() {
1290            // set sensors
1291            for (int k = 0; k < r.getNumOutputSensors(); k++) {
1292                Sensor t = r.getOutputSensor(k);
1293                if ( t==null ) {
1294                    setRouteLog.warn("Sensor {} not found for Route {}",k,r.getDisplayName());
1295                    continue;
1296                }
1297                int state = r.getOutputSensorState(k);
1298                if (state == Route.TOGGLE) {
1299                    int st = t.getKnownState();
1300                    if (st == Sensor.ACTIVE) {
1301                        state = Sensor.INACTIVE;
1302                    } else {
1303                        state = Sensor.ACTIVE;
1304                    }
1305                }
1306                final int toState = state;
1307                final Sensor setSensor = t;
1308                ThreadingUtil.runOnLayoutEventually(() -> { // eventually, even though timing here, should be soon
1309                    try {
1310                        setSensor.setKnownState(toState);
1311                    } catch (JmriException e) {
1312                        setRouteLog.warn("Exception setting sensor {} in route {} {}",
1313                            setSensor.getSystemName(), r.getDisplayName(), e.getMessage());
1314                    }
1315                });
1316                threadSleep(50);
1317            }
1318        }
1319
1320        private void threadSleep(long millis) {
1321            try {
1322                Thread.sleep(millis);
1323            } catch (InterruptedException e) {
1324                Thread.currentThread().interrupt(); // retain if needed later
1325            }
1326        }
1327
1328        private void setTurnouts() {
1329            // set turnouts
1330            int delay = r.getRouteCommandDelay();
1331
1332            for (int k = 0; k < r.getNumOutputTurnouts(); k++) {
1333                Turnout t = r.getOutputTurnout(k);
1334                if ( t == null ) {
1335                    log.error("No Turnout {} for Route {}",k,r.getDisplayName());
1336                    continue;
1337                }
1338                int state = r.getOutputTurnoutState(k);
1339                if (state == Route.TOGGLE) {
1340                    int st = t.getKnownState();
1341                    if (st == Turnout.CLOSED) {
1342                        state = Turnout.THROWN;
1343                    } else {
1344                        state = Turnout.CLOSED;
1345                    }
1346                }
1347                final int toState = state;
1348                final Turnout setTurnout = t;
1349                ThreadingUtil.runOnLayoutEventually(() -> // eventually, even though we have timing here
1350                    setTurnout.setCommandedStateAtInterval(toState)); // delayed by connection specific turnoutManager
1351                threadSleep(delay); // only the Route specific user defined delay is applied here
1352            }
1353        }
1354
1355        private final DefaultRoute r;
1356
1357        private static final Logger setRouteLog = LoggerFactory.getLogger(SetRouteThread.class);
1358    }
1359
1360}