001package jmri.implementation;
002
003import java.beans.*;
004import java.time.LocalDateTime;
005import java.time.temporal.ChronoUnit;
006import java.util.Arrays;
007import java.util.ArrayList;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Objects;
011import java.util.Set;
012
013import javax.annotation.*;
014
015import jmri.*;
016
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * Abstract base for the Turnout interface.
022 * <p>
023 * Implements basic feedback modes:
024 * <ul>
025 * <li>NONE feedback, where the KnownState and CommandedState track each other.
026 * <li>ONESENSOR feedback where the state of a single sensor specifies THROWN vs
027 * CLOSED
028 * <li>TWOSENSOR feedback, where one sensor specifies THROWN and another CLOSED.
029 * </ul>
030 * If you want to implement some other feedback, override and modify
031 * setCommandedState() here.
032 * <p>
033 * Implements the parameter binding support.
034 * <p>
035 * Note that we consider it an error for there to be more than one object that
036 * corresponds to a particular physical turnout on the layout.
037 *
038 * @author Bob Jacobsen Copyright (C) 2001, 2009
039 */
040public abstract class AbstractTurnout extends AbstractNamedBean implements
041        Turnout, PropertyChangeListener {
042
043    private Turnout leadingTurnout = null;
044    private boolean followingCommandedState = true;
045
046    protected AbstractTurnout(String systemName) {
047        super(systemName);
048    }
049
050    protected AbstractTurnout(String systemName, String userName) {
051        super(systemName, userName);
052    }
053
054    /** {@inheritDoc} */
055    @Override
056    @Nonnull
057    public String getBeanType() {
058        return Bundle.getMessage("BeanNameTurnout");
059    }
060
061    private final String closedText = InstanceManager.turnoutManagerInstance().getClosedText();
062    private final String thrownText = InstanceManager.turnoutManagerInstance().getThrownText();
063
064    /**
065     * Handle a request to change state, typically by sending a message to the
066     * layout in some child class. Public version (used by TurnoutOperator)
067     * sends the current commanded state without changing it.
068     * Implementing classes will typically check the value of s and send a system specific sendMessage command.
069     *
070     * @param s new state value
071     */
072    abstract protected void forwardCommandChangeToLayout(int s);
073
074    protected void forwardCommandChangeToLayout() {
075        forwardCommandChangeToLayout(_commandedState);
076    }
077
078    /**
079     * Preprocess a Turnout state change request for {@link #forwardCommandChangeToLayout(int)}
080     * Public access to allow use in tests.
081     *
082     * @param newState the Turnout state command value passed
083     * @return true if a Turnout.CLOSED was requested and Turnout is not set to _inverted
084     */
085    public boolean stateChangeCheck(int newState) throws IllegalArgumentException {
086        // sort out states
087        if ((newState & Turnout.CLOSED) != 0) {
088            if (statesOk(newState)) {
089                // request a CLOSED command (or THROWN if inverted)
090                return (!_inverted);
091            } else {
092                throw new IllegalArgumentException("Can't set state for Turnout " + newState);
093            }
094        }
095        // request a THROWN command (or CLOSED if inverted)
096        return (_inverted);
097    }
098
099    /**
100     * Look for the case in which the state is neither Closed nor Thrown, which we can't handle.
101     * Separate method to allow it to be used in {@link #stateChangeCheck} and Xpa/MqttTurnout.
102     *
103     * @param state the Turnout state passed
104     * @return false if s = Turnout.THROWN, which is what we want
105     */
106    protected boolean statesOk(int state) {
107        if ((state & Turnout.THROWN) != 0) {
108            // this is the disaster case!
109            log.error("Cannot command both CLOSED and THROWN");
110            return false;
111        }
112        return true;
113    }
114
115    /**
116     * Set a new Commanded state, if need be notifying the listeners, but do
117     * NOT send the command downstream.
118     * <p>
119     * This is used when a new commanded state
120     * is noticed from another command.
121     *
122     * @param s new state
123     */
124    protected void newCommandedState(int s) {
125        if (_commandedState != s) {
126            int oldState = _commandedState;
127            _commandedState = s;
128            firePropertyChange("CommandedState", oldState, _commandedState);
129        }
130    }
131
132    /** {@inheritDoc} */
133    @Override
134    public int getKnownState() {
135        return _knownState;
136    }
137
138    /**
139     * Public access to changing turnout state. Sets the commanded state and, if
140     * appropriate, starts a TurnoutOperator to do its thing. If there is no
141     * TurnoutOperator (not required or nothing suitable) then just tell the
142     * layout and hope for the best.
143     *
144     * @param s commanded state to set
145     */
146    @Override
147    public void setCommandedState(int s) {
148        log.debug("set commanded state for turnout {} to {}", getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME),
149                (s == Turnout.CLOSED ? closedText : thrownText));
150        newCommandedState(s);
151        myOperator = getTurnoutOperator(); // MUST set myOperator before starting the thread
152        if (myOperator == null) {
153            log.debug("myOperator NULL");
154            forwardCommandChangeToLayout(s);
155            // optionally handle feedback
156            if (_activeFeedbackType == DIRECT) {
157                newKnownState(s);
158            } else if (_activeFeedbackType == DELAYED) {
159                newKnownState(INCONSISTENT);
160                jmri.util.ThreadingUtil.runOnLayoutDelayed( () -> { newKnownState(s); },
161                         DELAYED_FEEDBACK_INTERVAL );
162            }
163        } else {
164            log.debug("myOperator NOT NULL");
165            myOperator.start();
166        }
167    }
168
169    /**
170     * Duration in Milliseconds of delay for DELAYED feedback mode.
171     * <p>
172     * Defined as "public non-final" so it can be changed in e.g.
173     * the jython/SetDefaultDelayedTurnoutDelay script.
174     */
175    public static int DELAYED_FEEDBACK_INTERVAL = 4000;
176
177    protected Thread thr;
178    protected Runnable r;
179    private LocalDateTime nextWait;
180
181    /** {@inheritDoc}
182     * Used in {@link jmri.implementation.DefaultRoute#setRoute()} and
183     * {@link jmri.implementation.MatrixSignalMast#updateOutputs(char[])}.
184     */
185    @Override
186    public void setCommandedStateAtInterval(int s) {
187        nextWait = InstanceManager.turnoutManagerInstance().outputIntervalEnds();
188        // nextWait time is calculated using actual turnoutInterval in TurnoutManager
189        if (nextWait.isAfter(LocalDateTime.now())) { // don't sleep if nextWait =< now()
190            log.debug("Turnout now() = {}, waitUntil = {}", LocalDateTime.now(), nextWait);
191            // insert wait before sending next output command to the layout
192            r = () -> {
193                log.debug("go to sleep for {} ms...", Math.max(0L, LocalDateTime.now().until(nextWait, ChronoUnit.MILLIS)));
194                try {
195                    Thread.sleep(Math.max(0L, LocalDateTime.now().until(nextWait, ChronoUnit.MILLIS))); // nextWait might have passed in the meantime
196                    log.debug("back from sleep, forward on {}", LocalDateTime.now());
197                    setCommandedState(s);
198                } catch (InterruptedException ex) {
199                    log.debug("setCommandedStateAtInterval(s) interrupted at {}", LocalDateTime.now());
200                    Thread.currentThread().interrupt(); // retain if needed later
201                }
202            };
203            thr = new Thread(r);
204            thr.setName("Turnout "+getDisplayName()+" setCommandedStateAtInterval");
205            thr.start();
206        } else {
207            log.debug("nextWait has passed");
208            setCommandedState(s);
209        }
210    }
211
212    /** {@inheritDoc} */
213    @Override
214    public int getCommandedState() {
215        return _commandedState;
216    }
217
218    /**
219     * Add a newKnownState() for use by implementations.
220     * <p>
221     * Use this to update internal information when a state change is detected
222     * <em>outside</em> the Turnout object, e.g. via feedback from sensors on
223     * the layout.
224     * <p>
225     * If the layout status of the Turnout is observed to change to THROWN or
226     * CLOSED, this also sets the commanded state, because it's assumed that
227     * somebody somewhere commanded that move. If it's observed to change to
228     * UNKNOWN or INCONSISTENT, that's perhaps either an error or a move in
229     * progress, and no change is made to the commanded state.
230     * <p>
231     * This implementation sends a command to the layout for the new state if
232     * going to THROWN or CLOSED, because there may be others listening to
233     * network state.
234     * <p>
235     * This method is not intended for general use, e.g. for users to set the
236     * KnownState, so it doesn't appear in the Turnout interface.
237     * <p>
238     * On change, fires Property Change "KnownState".
239     * @param s New state value
240     */
241    public void newKnownState(int s) {
242        if (_knownState != s) {
243            int oldState = _knownState;
244            _knownState = s;
245            firePropertyChange("KnownState", oldState, _knownState);
246        }
247        _knownState = s;
248        // if known state has moved to Thrown or Closed,
249        // set the commanded state to match
250        if ((_knownState == THROWN && _commandedState != THROWN)
251                || (_knownState == CLOSED && _commandedState != CLOSED)) {
252            newCommandedState(_knownState);
253        }
254    }
255
256    /**
257     * Show whether state is one you can safely run trains over.
258     *
259     * @return true if state is a valid one and the known state is the same as
260     *         commanded.
261     */
262    @Override
263    public boolean isConsistentState() {
264        return _commandedState == _knownState
265                && (_commandedState == CLOSED || _commandedState == THROWN);
266    }
267
268    /**
269     * The name pretty much says it.
270     * <p>
271     * Triggers all listeners, etc. For use by the TurnoutOperator classes.
272     */
273    void setKnownStateToCommanded() {
274        newKnownState(_commandedState);
275    }
276
277    /**
278     * Implement a shorter name for setCommandedState.
279     * <p>
280     * This generally shouldn't be used by Java code; use setCommandedState
281     * instead. The is provided to make Jython script access easier to read.
282     * <p>
283     * Note that getState() and setState(int) are not symmetric: getState is the
284     * known state, and set state modifies the commanded state.
285     * @param s new state
286     */
287    @Override
288    public void setState(int s) {
289        setCommandedState(s);
290    }
291
292    /**
293     * Implement a shorter name for getKnownState.
294     * <p>
295     * This generally shouldn't be used by Java code; use getKnownState instead.
296     * The is provided to make Jython script access easier to read.
297     * <p>
298     * Note that getState() and setState(int) are not symmetric: getState is the
299     * known state, and set state modifies the commanded state.
300     * @return current state
301     */
302    @Override
303    public int getState() {
304        return getKnownState();
305    }
306
307    /** {@inheritDoc} */
308    @Override
309    @Nonnull
310    public String describeState(int state) {
311        switch (state) {
312            case THROWN: return Bundle.getMessage("TurnoutStateThrown");
313            case CLOSED: return Bundle.getMessage("TurnoutStateClosed");
314            default: return super.describeState(state);
315        }
316    }
317
318    protected String[] _validFeedbackNames = {"DIRECT", "ONESENSOR",
319        "TWOSENSOR", "DELAYED"};
320
321    protected int[] _validFeedbackModes = {DIRECT, ONESENSOR, TWOSENSOR, DELAYED};
322
323    protected int _validFeedbackTypes = DIRECT | ONESENSOR | TWOSENSOR | DELAYED;
324
325    protected int _activeFeedbackType = DIRECT;
326
327    private int _knownState = UNKNOWN;
328
329    private int _commandedState = UNKNOWN;
330
331    private int _numberControlBits = 1;
332
333    /** Number of bits to control a turnout - defaults to one */
334    private int _controlType = 0;
335
336    /** Type of turnout control - defaults to 0 for /'steady state/' */
337    @Override
338    public int getNumberControlBits() {
339        return _numberControlBits;
340    }
341
342    /** {@inheritDoc} */
343    @Override
344    public void setNumberControlBits(int num) {
345        _numberControlBits = num;
346    }
347
348    /** {@inheritDoc} */
349    @Override
350    public int getControlType() {
351        return _controlType;
352    }
353
354    /** {@inheritDoc} */
355    @Override
356    public void setControlType(int num) {
357        _controlType = num;
358    }
359
360    /** {@inheritDoc} */
361    @Override
362    public Set<Integer> getValidFeedbackModes() {
363        Set<Integer> modes = new HashSet<>();
364        Arrays.stream(_validFeedbackModes).forEach(modes::add);
365        return modes;
366    }
367
368    /** {@inheritDoc} */
369    @Override
370    public int getValidFeedbackTypes() {
371        return _validFeedbackTypes;
372    }
373
374    /** {@inheritDoc} */
375    @Override
376    @Nonnull
377    public String[] getValidFeedbackNames() {
378        return Arrays.copyOf(_validFeedbackNames, _validFeedbackNames.length);
379    }
380
381    /** {@inheritDoc} */
382    @Override
383    public void setFeedbackMode(@Nonnull String mode) throws IllegalArgumentException {
384        for (int i = 0; i < _validFeedbackNames.length; i++) {
385            if (mode.equals(_validFeedbackNames[i])) {
386                setFeedbackMode(_validFeedbackModes[i]);
387                setInitialKnownStateFromFeedback();
388                return;
389            }
390        }
391        throw new IllegalArgumentException("Unexpected mode: " + mode);
392    }
393
394    /**
395     * On change, fires Property Change "feedbackchange".
396     * {@inheritDoc}
397     */
398    @Override
399    public void setFeedbackMode(int mode) throws IllegalArgumentException {
400        // check for error - following removed the low bit from mode
401        int test = mode & (mode - 1);
402        if (test != 0) {
403            throw new IllegalArgumentException("More than one bit set: " + mode);
404        }
405        // set the value
406        int oldMode = _activeFeedbackType;
407        _activeFeedbackType = mode;
408        // unlock turnout if feedback is changed
409        setLocked(CABLOCKOUT, false);
410        if (oldMode != _activeFeedbackType) {
411            firePropertyChange("feedbackchange", oldMode,
412                    _activeFeedbackType);
413        }
414    }
415
416    /** {@inheritDoc} */
417    @Override
418    public int getFeedbackMode() {
419        return _activeFeedbackType;
420    }
421
422    /** {@inheritDoc} */
423    @Override
424    @Nonnull
425    public String getFeedbackModeName() {
426        for (int i = 0; i < _validFeedbackNames.length; i++) {
427            if (_activeFeedbackType == _validFeedbackModes[i]) {
428                return _validFeedbackNames[i];
429            }
430        }
431        throw new IllegalArgumentException("Unexpected internal mode: "
432                + _activeFeedbackType);
433    }
434
435    /** {@inheritDoc} */
436    @Override
437    public void requestUpdateFromLayout() {
438        if (_activeFeedbackType == ONESENSOR || _activeFeedbackType == TWOSENSOR) {
439            Sensor s1 = getFirstSensor();
440            if (s1 != null) s1.requestUpdateFromLayout();
441        }
442        if (_activeFeedbackType == TWOSENSOR) {
443            Sensor s2 = getSecondSensor();
444            if (s2 != null) s2.requestUpdateFromLayout();
445        }
446    }
447
448    /**
449     * On change, fires Property Change "inverted".
450     * {@inheritDoc}
451     */
452    @Override
453    public void setInverted(boolean inverted) {
454        boolean oldInverted = _inverted;
455        _inverted = inverted;
456        if (oldInverted != _inverted) {
457            int state = _knownState;
458            if (state == THROWN) {
459                newKnownState(CLOSED);
460            } else if (state == CLOSED) {
461                newKnownState(THROWN);
462            }
463            firePropertyChange("inverted", oldInverted, _inverted);
464        }
465    }
466
467    /**
468     * Get the turnout inverted state. If true, commands sent to the layout are
469     * reversed. Thrown becomes Closed, and Closed becomes Thrown.
470     * <p>
471     * Used in polling loops in system-specific code, so made final to allow
472     * optimization.
473     *
474     * @return inverted status
475     */
476    @Override
477    final public boolean getInverted() {
478        return _inverted;
479    }
480
481    protected boolean _inverted = false;
482
483    /**
484     * Determine if the turnouts can be inverted. If true, inverted turnouts
485     * are supported.
486     * @return invert supported
487     */
488    @Override
489    public boolean canInvert() {
490        return false;
491    }
492
493    /**
494     * Turnouts that are locked should only respond to JMRI commands to change
495     * state.
496     * We simulate a locked turnout by monitoring the known state (turnout
497     * feedback is required) and if we detect that the known state has
498     * changed,
499     * negate it by forcing the turnout to return to the commanded
500     * state.
501     * Turnouts that have local buttons can also be locked if their
502     * decoder supports it.
503     * On change, fires Property Change "locked".
504     *
505     * @param turnoutLockout lockout state to monitor. Possible values
506     *                       {@link #CABLOCKOUT}, {@link #PUSHBUTTONLOCKOUT}.
507     *                       Can be combined to monitor both states.
508     * @param locked         true if turnout to be locked
509     */
510    @Override
511    public void setLocked(int turnoutLockout, boolean locked) {
512        boolean firechange = false;
513        if ((turnoutLockout & CABLOCKOUT) != 0 && _cabLockout != locked) {
514            firechange = true;
515            if (canLock(CABLOCKOUT)) {
516                _cabLockout = locked;
517            } else {
518                _cabLockout = false;
519            }
520        }
521        if ((turnoutLockout & PUSHBUTTONLOCKOUT) != 0
522                && _pushButtonLockout != locked) {
523            firechange = true;
524            if (canLock(PUSHBUTTONLOCKOUT)) {
525                _pushButtonLockout = locked;
526                // now change pushbutton lockout state on layout
527                turnoutPushbuttonLockout();
528            } else {
529                _pushButtonLockout = false;
530            }
531        }
532        if (firechange) {
533            firePropertyChange("locked", !locked, locked);
534        }
535    }
536
537    /**
538     * Determine if turnout is locked. There
539     * are two types of locks: cab lockout, and pushbutton lockout.
540     *
541     * @param turnoutLockout turnout to check
542     * @return locked state, true if turnout is locked
543     */
544    @Override
545    public boolean getLocked(int turnoutLockout) {
546        switch (turnoutLockout) {
547            case CABLOCKOUT:
548                return _cabLockout;
549            case PUSHBUTTONLOCKOUT:
550                return _pushButtonLockout;
551            case CABLOCKOUT + PUSHBUTTONLOCKOUT:
552                return _cabLockout || _pushButtonLockout;
553            default:
554                return false;
555        }
556    }
557
558    protected boolean _cabLockout = false;
559
560    protected boolean _pushButtonLockout = false;
561
562    protected boolean _enableCabLockout = false;
563
564    protected boolean _enablePushButtonLockout = false;
565
566    /**
567     * This implementation by itself doesn't provide locking support.
568     * Override this in subclasses that do.
569     *
570     * @return One of 0 for none
571     */
572    @Override
573    public int getPossibleLockModes() { return 0; }
574
575    /**
576     * This implementation by itself doesn't provide locking support.
577     * Override this in subclasses that do.
578     *
579     * @return false for not supported
580     */
581    @Override
582    public boolean canLock(int turnoutLockout) {
583        return false;
584    }
585
586    /** {@inheritDoc}
587     * Not implemented in AbstractTurnout.
588     */
589    @Override
590    public void enableLockOperation(int turnoutLockout, boolean enabled) {
591    }
592
593    /**
594     * When true, report to console anytime a cab attempts to change the state
595     * of a turnout on the layout.
596     * When a turnout is cab locked, only JMRI is
597     * allowed to change the state of a turnout.
598     * On setting changed, fires Property Change "reportlocked".
599     *
600     * @param reportLocked report locked state
601     */
602    @Override
603    public void setReportLocked(boolean reportLocked) {
604        boolean oldReportLocked = _reportLocked;
605        _reportLocked = reportLocked;
606        if (oldReportLocked != _reportLocked) {
607            firePropertyChange("reportlocked", oldReportLocked,
608                    _reportLocked);
609        }
610    }
611
612    /**
613     * When true, report to console anytime a cab attempts to change the state
614     * of a turnout on the layout. When a turnout is cab locked, only JMRI is
615     * allowed to change the state of a turnout.
616     *
617     * @return report locked state
618     */
619    @Override
620    public boolean getReportLocked() {
621        return _reportLocked;
622    }
623
624    protected boolean _reportLocked = true;
625
626    /**
627     * Valid stationary decoder names.
628     */
629    protected String[] _validDecoderNames = PushbuttonPacket
630            .getValidDecoderNames();
631
632    /** {@inheritDoc} */
633    @Override
634    @Nonnull
635    public String[] getValidDecoderNames() {
636        return Arrays.copyOf(_validDecoderNames, _validDecoderNames.length);
637    }
638
639    // set the turnout decoder default to unknown
640    protected String _decoderName = PushbuttonPacket.unknown;
641
642    /** {@inheritDoc} */
643    @Override
644    public String getDecoderName() {
645        return _decoderName;
646    }
647
648    /**
649     * {@inheritDoc}
650     * On change, fires Property Change "decoderNameChange".
651     */
652    @Override
653    public void setDecoderName(final String decoderName) {
654        if (!(Objects.equals(_decoderName, decoderName))) {
655            String oldName = _decoderName;
656            _decoderName = decoderName;
657            firePropertyChange("decoderNameChange", oldName, decoderName);
658        }
659    }
660
661    abstract protected void turnoutPushbuttonLockout(boolean locked);
662
663    protected void turnoutPushbuttonLockout() {
664        turnoutPushbuttonLockout(_pushButtonLockout);
665    }
666
667    /*
668     * Support for turnout automation (see TurnoutOperation and related classes).
669     */
670    protected TurnoutOperator myOperator;
671
672    protected TurnoutOperation myTurnoutOperation;
673
674    protected boolean inhibitOperation = true; // do not automate this turnout, even if globally operations are on
675
676    public TurnoutOperator getCurrentOperator() {
677        return myOperator;
678    }
679
680    /** {@inheritDoc} */
681    @Override
682    public TurnoutOperation getTurnoutOperation() {
683        return myTurnoutOperation;
684    }
685
686    /**
687     * {@inheritDoc}
688     * Fires Property Change "TurnoutOperationState".
689     */
690    @Override
691    public void setTurnoutOperation(TurnoutOperation toper) {
692        log.debug("setTurnoutOperation Called for turnout {}.  Operation type {}", this.getSystemName(), toper);
693        TurnoutOperation oldOp = myTurnoutOperation;
694        if (myTurnoutOperation != null) {
695            myTurnoutOperation.removePropertyChangeListener(this);
696        }
697        myTurnoutOperation = toper;
698        if (myTurnoutOperation != null) {
699            myTurnoutOperation.addPropertyChangeListener(this);
700        }
701        firePropertyChange("TurnoutOperationState", oldOp, myTurnoutOperation);
702    }
703
704    protected void operationPropertyChange(java.beans.PropertyChangeEvent evt) {
705        if (evt.getSource() == myTurnoutOperation) {
706            if (((TurnoutOperation) evt.getSource()).isDeleted()) {
707                setTurnoutOperation(null);
708            }
709        }
710    }
711
712    /** {@inheritDoc} */
713    @Override
714    public boolean getInhibitOperation() {
715        return inhibitOperation;
716    }
717
718    /** {@inheritDoc} */
719    @Override
720    public void setInhibitOperation(boolean io) {
721        inhibitOperation = io;
722    }
723
724    /**
725     * Find the TurnoutOperation class for this turnout, and get an instance of
726     * the corresponding operator. Override this function if you want another way
727     * to choose the operation.
728     *
729     * @return newly-instantiated TurnoutOperator, or null if nothing suitable
730     */
731    protected TurnoutOperator getTurnoutOperator() {
732        TurnoutOperator to = null;
733        if (!inhibitOperation) {
734            if (myTurnoutOperation != null) {
735                to = myTurnoutOperation.getOperator(this);
736            } else {
737                TurnoutOperation toper = InstanceManager.getDefault(TurnoutOperationManager.class)
738                        .getMatchingOperation(this,
739                                getFeedbackModeForOperation());
740                if (toper != null) {
741                    to = toper.getOperator(this);
742                }
743            }
744        }
745        return to;
746    }
747
748    /**
749     * Allow an actual turnout class to transform private feedback types into
750     * ones that the generic turnout operations know about.
751     *
752     * @return    apparent feedback mode for operation lookup
753     */
754    protected int getFeedbackModeForOperation() {
755        return getFeedbackMode();
756    }
757
758    /**
759     * Support for associated sensor or sensors.
760     */
761    //Sensor getFirstSensor() = null;
762    private NamedBeanHandle<Sensor> _firstNamedSensor;
763
764    //Sensor getSecondSensor() = null;
765    private NamedBeanHandle<Sensor> _secondNamedSensor;
766
767    /** {@inheritDoc} */
768    @Override
769    public void provideFirstFeedbackSensor(String pName) throws jmri.JmriException, IllegalArgumentException {
770        if (InstanceManager.getNullableDefault(SensorManager.class) != null) {
771            if (pName == null || pName.isEmpty()) {
772                provideFirstFeedbackNamedSensor(null);
773            } else {
774                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
775                provideFirstFeedbackNamedSensor(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
776            }
777        } else {
778            log.error("No SensorManager for this protocol");
779            throw new jmri.JmriException("No Sensor Manager Found");
780        }
781    }
782
783    /**
784     * On change, fires Property Change "TurnoutFeedbackFirstSensorChange".
785     * @param s the Handle for First Feedback Sensor
786     */
787    public void provideFirstFeedbackNamedSensor(NamedBeanHandle<Sensor> s) {
788        // remove existing if any
789        Sensor temp = getFirstSensor();
790        if (temp != null) {
791            temp.removePropertyChangeListener(this);
792        }
793
794        _firstNamedSensor = s;
795
796        // if need be, set listener
797        temp = getFirstSensor();  // might have changed
798        if (temp != null) {
799            temp.addPropertyChangeListener(this, s.getName(), "Feedback Sensor for " + getDisplayName());
800        }
801        // set initial state
802        setInitialKnownStateFromFeedback();
803        firePropertyChange("turnoutFeedbackFirstSensorChange", temp, s);
804    }
805
806    /** {@inheritDoc} */
807    @Override
808    public Sensor getFirstSensor() {
809        if (_firstNamedSensor == null) {
810            return null;
811        }
812        return _firstNamedSensor.getBean();
813    }
814
815    /** {@inheritDoc} */
816    @Override
817    public NamedBeanHandle<Sensor> getFirstNamedSensor() {
818        return _firstNamedSensor;
819    }
820
821    /** {@inheritDoc} */
822    @Override
823    public void provideSecondFeedbackSensor(String pName) throws jmri.JmriException, IllegalArgumentException {
824        if (InstanceManager.getNullableDefault(SensorManager.class) != null) {
825            if (pName == null || pName.isEmpty()) {
826                provideSecondFeedbackNamedSensor(null);
827            } else {
828                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
829                provideSecondFeedbackNamedSensor(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
830            }
831        } else {
832            log.error("No SensorManager for this protocol");
833            throw new jmri.JmriException("No Sensor Manager Found");
834        }
835    }
836
837    /**
838     * On change, fires Property Change "TurnoutFeedbackSecondSensorChange".
839     * @param s the Handle for Second Feedback Sensor
840     */
841    public void provideSecondFeedbackNamedSensor(NamedBeanHandle<Sensor> s) {
842        // remove existing if any
843        Sensor temp = getSecondSensor();
844        if (temp != null) {
845            temp.removePropertyChangeListener(this);
846        }
847
848        _secondNamedSensor = s;
849
850        // if need be, set listener
851        temp = getSecondSensor();  // might have changed
852        if (temp != null) {
853            temp.addPropertyChangeListener(this, s.getName(), "Feedback Sensor for " + getDisplayName());
854        }
855        // set initial state
856        setInitialKnownStateFromFeedback();
857        firePropertyChange("turnoutFeedbackSecondSensorChange", temp, s);
858    }
859
860    /** {@inheritDoc} */
861    @CheckForNull
862    @Override
863    public Sensor getSecondSensor() {
864        if (_secondNamedSensor == null) {
865            return null;
866        }
867        return _secondNamedSensor.getBean();
868    }
869
870    /** {@inheritDoc} */
871    @CheckForNull
872    @Override
873    public NamedBeanHandle<Sensor> getSecondNamedSensor() {
874        return _secondNamedSensor;
875    }
876
877    /** {@inheritDoc} */
878    @Override
879    public void setInitialKnownStateFromFeedback() {
880        Sensor firstSensor = getFirstSensor();
881        if (_activeFeedbackType == ONESENSOR) {
882            // ONESENSOR feedback
883            if (firstSensor != null) {
884                // set according to state of sensor
885                int sState = firstSensor.getKnownState();
886                if (sState == Sensor.ACTIVE) {
887                    newKnownState(THROWN);
888                } else if (sState == Sensor.INACTIVE) {
889                    newKnownState(CLOSED);
890                }
891            } else {
892                log.warn("expected Sensor 1 not defined - {}", getSystemName());
893                newKnownState(UNKNOWN);
894            }
895        } else if (_activeFeedbackType == TWOSENSOR) {
896            // TWOSENSOR feedback
897            int s1State = Sensor.UNKNOWN;
898            int s2State = Sensor.UNKNOWN;
899            if (firstSensor != null) {
900                s1State = firstSensor.getKnownState();
901            } else {
902                log.warn("expected Sensor 1 not defined - {}", getSystemName());
903            }
904            Sensor secondSensor = getSecondSensor();
905            if (secondSensor != null) {
906                s2State = secondSensor.getKnownState();
907            } else {
908                log.warn("expected Sensor 2 not defined - {}", getSystemName());
909            }
910            // set Turnout state according to sensors
911            if ((s1State == Sensor.ACTIVE) && (s2State == Sensor.INACTIVE)) {
912                newKnownState(THROWN);
913            } else if ((s1State == Sensor.INACTIVE) && (s2State == Sensor.ACTIVE)) {
914                newKnownState(CLOSED);
915            } else if (_knownState != UNKNOWN) {
916                newKnownState(UNKNOWN);
917            }
918        // nothing required at this time for other modes
919        }
920    }
921
922    /**
923     * React to sensor changes by changing the KnownState if using an
924     * appropriate sensor mode.
925     */
926    @Override
927    public void propertyChange(PropertyChangeEvent evt) {
928        if (evt.getSource() == myTurnoutOperation) {
929            operationPropertyChange(evt);
930        } else if (evt.getSource() == getFirstSensor()
931                || evt.getSource() == getSecondSensor()) {
932            sensorPropertyChange(evt);
933        } else if (evt.getSource() == leadingTurnout) {
934            leadingTurnoutPropertyChange(evt);
935        }
936    }
937
938    protected void sensorPropertyChange(PropertyChangeEvent evt) {
939        // top level, find the mode
940        Sensor src = (Sensor) evt.getSource();
941        Sensor s1 = getFirstSensor();
942        if (src == null || s1 == null) {
943            log.warn("Turnout feedback sensors configured incorrectly ");
944            return; // can't complete
945        }
946
947        if (_activeFeedbackType == ONESENSOR) {
948            // check for match
949            if (src == s1) {
950                // check change type
951                if (!evt.getPropertyName().equals("KnownState")) {
952                    return;
953                }
954                // OK, now handle it
955                switch ((Integer) evt.getNewValue()) {
956                    case Sensor.ACTIVE:
957                        newKnownState(THROWN);
958                        break;
959                    case Sensor.INACTIVE:
960                        newKnownState(CLOSED);
961                        break;
962                    default:
963                        newKnownState(INCONSISTENT);
964                        break;
965                }
966            } else {
967                // unexpected mismatch
968                NamedBeanHandle<Sensor> firstNamed = getFirstNamedSensor();
969                if (firstNamed != null) {
970                    log.warn("expected sensor {} was {}", firstNamed.getName(), src.getSystemName());
971                } else {
972                    log.error("unexpected (null) sensors");
973                }
974            }
975            // end ONESENSOR block
976        } else if (_activeFeedbackType == TWOSENSOR) {
977            // check change type
978            if (!evt.getPropertyName().equals("KnownState")) {
979                return;
980            }
981            // OK, now handle it
982            Sensor s2 = getSecondSensor();
983            if (s2 == null) {
984                log.warn("Turnout feedback sensor 2 configured incorrectly ");
985                return; // can't complete
986            }
987            if (s1.getKnownState() == Sensor.INACTIVE && s2.getKnownState() == Sensor.ACTIVE) {
988                newKnownState(CLOSED);
989            } else if (s1.getKnownState() == Sensor.ACTIVE && s2.getKnownState() == Sensor.INACTIVE) {
990                newKnownState(THROWN);
991            } else if (s1.getKnownState() == Sensor.UNKNOWN && s2.getKnownState() == Sensor.UNKNOWN) {
992                newKnownState(UNKNOWN);
993            } else {
994                newKnownState(INCONSISTENT);
995            }
996            // end TWOSENSOR block
997        }
998    }
999
1000    protected void leadingTurnoutPropertyChange(PropertyChangeEvent evt) {
1001        int state = (int) evt.getNewValue();
1002        if ("KnownState".equals(evt.getPropertyName())
1003                && leadingTurnout != null) {
1004            if (followingCommandedState || state != leadingTurnout.getCommandedState()) {
1005                newKnownState(state);
1006            } else {
1007                newKnownState(getCommandedState());
1008            }
1009        }
1010    }
1011
1012    /** {@inheritDoc} */
1013    @Override
1014    public void setBinaryOutput(boolean state) {
1015        binaryOutput = true;
1016    }
1017    protected boolean binaryOutput = false;
1018
1019    /** {@inheritDoc} */
1020    @Override
1021    public void dispose() {
1022        Sensor temp;
1023        temp = getFirstSensor();
1024        if (temp != null) {
1025            temp.removePropertyChangeListener(this);
1026        }
1027        _firstNamedSensor = null;
1028        temp = getSecondSensor();
1029        if (temp != null) {
1030            temp.removePropertyChangeListener(this);
1031        }
1032        _secondNamedSensor = null;
1033        super.dispose();
1034    }
1035
1036    private String _divergeSpeed = "";
1037    private String _straightSpeed = "";
1038    // private boolean useBlockSpeed = true;
1039    // private float speedThroughTurnout = 0;
1040
1041    /** {@inheritDoc} */
1042    @Override
1043    public float getDivergingLimit() {
1044        if ((_divergeSpeed == null) || (_divergeSpeed.isEmpty())) {
1045            return -1;
1046        }
1047
1048        String speed = _divergeSpeed;
1049        if (_divergeSpeed.equals("Global")) {
1050            speed = InstanceManager.turnoutManagerInstance().getDefaultThrownSpeed();
1051        }
1052        if (speed.equals("Block")) {
1053            return -1;
1054        }
1055        try {
1056            return Float.parseFloat(speed);
1057            //return Integer.parseInt(_blockSpeed);
1058        } catch (NumberFormatException nx) {
1059            //considered normal if the speed is not a number.
1060        }
1061        try {
1062            return jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
1063        } catch (IllegalArgumentException ex) {
1064            return -1;
1065        }
1066    }
1067
1068    /** {@inheritDoc} */
1069    @Override
1070    public String getDivergingSpeed() {
1071        if (_divergeSpeed.equals("Global")) {
1072            return (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.turnoutManagerInstance().getDefaultThrownSpeed());
1073        }
1074        if (_divergeSpeed.equals("Block")) {
1075            return (Bundle.getMessage("UseGlobal", "Block Speed"));
1076        }
1077        return _divergeSpeed;
1078    }
1079
1080    /**
1081     * {@inheritDoc}
1082     * On change, fires Property Change "TurnoutDivergingSpeedChange".
1083     */
1084    @Override
1085    public void setDivergingSpeed(String s) throws JmriException {
1086        if (s == null) {
1087            throw new JmriException("Value of requested turnout thrown speed can not be null");
1088        }
1089        if (_divergeSpeed.equals(s)) {
1090            return;
1091        }
1092        if (s.contains("Global")) {
1093            s = "Global";
1094        } else if (s.contains("Block")) {
1095            s = "Block";
1096        } else {
1097            try {
1098                Float.parseFloat(s);
1099            } catch (NumberFormatException nx) {
1100                try {
1101                    jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s);
1102                } catch (IllegalArgumentException ex) {
1103                    throw new JmriException("Value of requested block speed is not valid");
1104                }
1105            }
1106        }
1107        String oldSpeed = _divergeSpeed;
1108        _divergeSpeed = s;
1109        firePropertyChange("TurnoutDivergingSpeedChange", oldSpeed, s);
1110    }
1111
1112    /** {@inheritDoc} */
1113    @Override
1114    public float getStraightLimit() {
1115        if ((_straightSpeed == null) || (_straightSpeed.isEmpty())) {
1116            return -1;
1117        }
1118        String speed = _straightSpeed;
1119        if (_straightSpeed.equals("Global")) {
1120            speed = InstanceManager.turnoutManagerInstance().getDefaultClosedSpeed();
1121        }
1122        if (speed.equals("Block")) {
1123            return -1;
1124        }
1125        try {
1126            return Float.parseFloat(speed);
1127        } catch (NumberFormatException nx) {
1128            //considered normal if the speed is not a number.
1129        }
1130        try {
1131            return jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
1132        } catch (IllegalArgumentException ex) {
1133            return -1;
1134        }
1135    }
1136
1137    /** {@inheritDoc} */
1138    @Override
1139    public String getStraightSpeed() {
1140        if (_straightSpeed.equals("Global")) {
1141            return (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.turnoutManagerInstance().getDefaultClosedSpeed());
1142        }
1143        if (_straightSpeed.equals("Block")) {
1144            return (Bundle.getMessage("UseGlobal", "Block Speed"));
1145        }
1146        return _straightSpeed;
1147    }
1148
1149    /**
1150     * {@inheritDoc}
1151     * On change, fires Property Change "TurnoutStraightSpeedChange".
1152     */
1153    @Override
1154    public void setStraightSpeed(String s) throws JmriException {
1155        if (s == null) {
1156            throw new JmriException("Value of requested turnout straight speed can not be null");
1157        }
1158        if (_straightSpeed.equals(s)) {
1159            return;
1160        }
1161        if (s.contains("Global")) {
1162            s = "Global";
1163        } else if (s.contains("Block")) {
1164            s = "Block";
1165        } else {
1166            try {
1167                Float.parseFloat(s);
1168            } catch (NumberFormatException nx) {
1169                try {
1170                    jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s);
1171                } catch (IllegalArgumentException ex) {
1172                    throw new JmriException("Value of requested turnout straight speed is not valid");
1173                }
1174            }
1175        }
1176        String oldSpeed = _straightSpeed;
1177        _straightSpeed = s;
1178        firePropertyChange("TurnoutStraightSpeedChange", oldSpeed, s);
1179    }
1180
1181    /** {@inheritDoc} */
1182    @Override
1183    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
1184        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
1185            Object old = evt.getOldValue();
1186            if (old.equals(getFirstSensor()) || old.equals(getSecondSensor()) || old.equals(leadingTurnout)) {
1187                java.beans.PropertyChangeEvent e = new java.beans.PropertyChangeEvent(this, "DoNotDelete", null, null);
1188                throw new java.beans.PropertyVetoException(Bundle.getMessage("InUseSensorTurnoutVeto", getDisplayName()), e); // NOI18N
1189            }
1190        }
1191    }
1192
1193    /** {@inheritDoc} */
1194    @Override
1195    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1196        List<NamedBeanUsageReport> report = new ArrayList<>();
1197        if (bean != null) {
1198            if (bean.equals(getFirstSensor())) {
1199                report.add(new NamedBeanUsageReport("TurnoutFeedback1"));  // NOI18N
1200            }
1201            if (bean.equals(getSecondSensor())) {
1202                report.add(new NamedBeanUsageReport("TurnoutFeedback2"));  // NOI18N
1203            }
1204            if (bean.equals(getLeadingTurnout())) {
1205                report.add(new NamedBeanUsageReport("LeadingTurnout")); // NOI18N
1206            }
1207        }
1208        return report;
1209    }
1210
1211    /**
1212     * {@inheritDoc}
1213     */
1214    @Override
1215    public boolean isCanFollow() {
1216        return false;
1217    }
1218
1219    /**
1220     * {@inheritDoc}
1221     */
1222    @Override
1223    @CheckForNull
1224    public Turnout getLeadingTurnout() {
1225        return leadingTurnout;
1226    }
1227
1228    /**
1229     * {@inheritDoc}
1230     */
1231    @Override
1232    public void setLeadingTurnout(@CheckForNull Turnout turnout) {
1233        if (isCanFollow()) {
1234            Turnout old = leadingTurnout;
1235            leadingTurnout = turnout;
1236            firePropertyChange("LeadingTurnout", old, leadingTurnout);
1237            if (old != null) {
1238                old.removePropertyChangeListener("KnownState", this);
1239            }
1240            if (leadingTurnout != null) {
1241                leadingTurnout.addPropertyChangeListener("KnownState", this);
1242            }
1243        }
1244    }
1245
1246    /**
1247     * {@inheritDoc}
1248     */
1249    @Override
1250    public void setLeadingTurnout(@CheckForNull Turnout turnout, boolean followingCommandedState) {
1251        setLeadingTurnout(turnout);
1252        setFollowingCommandedState(followingCommandedState);
1253    }
1254
1255    /**
1256     * {@inheritDoc}
1257     */
1258    @Override
1259    public boolean isFollowingCommandedState() {
1260        return followingCommandedState;
1261    }
1262
1263    /**
1264     * {@inheritDoc}
1265     */
1266    @Override
1267    public void setFollowingCommandedState(boolean following) {
1268        followingCommandedState = following;
1269    }
1270
1271    private final static Logger log = LoggerFactory.getLogger(AbstractTurnout.class);
1272
1273}