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