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