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