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