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