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