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