001/*
002 *  @author Gregory J. Bedlek Copyright (C) 2018, 2019
003 */
004package jmri.jmrit.ctc;
005
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import java.util.ArrayList;
009import jmri.Sensor;
010import jmri.Turnout;
011import jmri.jmrit.ctc.ctcserialdata.CodeButtonHandlerData;
012
013/**
014This only works with Digitrax DS54's and DS64's configured to LOCALLY change the switch via either
015a pushbutton or toggle switch.  Specifically in JMRI / DS64 programmer, OpSw 21 SHOULD
016be checked.  DS54's should be similarly configured.
017
018The other way:
019One would NOT check OpSw21, and then one would have to write JMRI software (or a script)
020to process the message from the DS54/DS64, and then send the appropriate turnout
021"CLOSED/THROWN" command to the turnout to effect the change.
022
023Advantage:      No turnout movement when crew requests change unless allowed by Dispatcher.
024Disadvantage:   Software MUST be running in order to throw turnout "normally".  In other
025                words cannot run the layout without the computer, and with all turnouts
026                controlled by the dispatcher set to "local control".
027
028This modules way:
029The purpose of this module is to "countermand" attempts by the field crews to throw
030a switch that is under dispatcher control.  This works ONLY for switches that have feedback.
031
032Advantage:      Computer NOT necessary to throw switch.
033Disadvantage:   Switch will "partially throw" until the feedback contact changes and sends
034                a message to the software, which then countermands it.  If a train is on the
035                switch, it may be derailed.
036
037NOTES:
038    If a route is cleared thru, you MUST be prevented from UNLOCKING a locked switch.
039Failure to provide such an object will just allow unlocking while a route is cleared thru.
040
041If dispatcherSensorLockToggle is None, then INSURE that you call "ExternalLockTurnout" at some
042point to lock the turnout, since this starts up with the turnout unlocked!
043
044* See the documentation for the matrix regarding Command and Feedback normal/reversed.
045 */
046
047public class TurnoutLock {
048    private final NBHSensor _mDispatcherSensorLockToggle;
049    private int _mCommandedState = Turnout.CLOSED;  // Assume
050    private ArrayList<NBHTurnout> _mTurnoutsMonitored = new ArrayList<>();
051    private PropertyChangeListener _mTurnoutsMonitoredPropertyChangeListener = null;
052    private boolean _mLocked = false;
053    private NBHSensor _mDispatcherSensorUnlockedIndicator;
054    private boolean _mNoDispatcherControlOfSwitch = false;
055    private int _m_ndcos_WhenLockedSwitchState = 0;
056
057    public TurnoutLock( String userIdentifier,
058                        NBHSensor dispatcherSensorLockToggle,          // Toggle switch that indicates lock/unlock on the panel.  If None, then PERMANENTLY locked by the Dispatcher!
059                        NBHTurnout actualTurnout,                       // The turnout being locked: LTxx a real turnout, like LT69.
060                        boolean actualTurnoutFeedbackDifferent,     // True / False, in case feedback backwards but switch command above isn't!
061                        NBHSensor dispatcherSensorUnlockedIndicator,   // Display unlocked status (when ACTIVE) back to the Dispatcher.
062                        boolean noDispatcherControlOfSwitch,        // Dispatcher doesn't control the switch.  If TRUE, then provide:
063                        int ndcos_WhenLockedSwitchState,            // When Dispatcher does lock, switch should be set to: CLOSED/THROWN
064                        CodeButtonHandlerData.LOCK_IMPLEMENTATION _mLockImplementation,  // Someday, choose which one to implement.  Right now, my own.
065                        boolean turnoutLocksEnabledAtStartup,
066                        NBHTurnout additionalTurnout1,
067                        boolean additionalTurnout1FeebackReversed,
068                        NBHTurnout additionalTurnout2,
069                        boolean additionalTurnout2FeebackReversed,
070                        NBHTurnout additionalTurnout3,
071                        boolean additionalTurnout3FeebackReversed) {
072        _mDispatcherSensorLockToggle = dispatcherSensorLockToggle;
073        addTurnoutMonitored(userIdentifier, "actualTurnout", actualTurnout, actualTurnoutFeedbackDifferent, true);
074        _mDispatcherSensorUnlockedIndicator = dispatcherSensorUnlockedIndicator;
075        _mDispatcherSensorLockToggle.setKnownState(turnoutLocksEnabledAtStartup ? Sensor.INACTIVE : Sensor.ACTIVE);
076        _mNoDispatcherControlOfSwitch = noDispatcherControlOfSwitch;
077        _m_ndcos_WhenLockedSwitchState = ndcos_WhenLockedSwitchState;
078        addTurnoutMonitored(userIdentifier, "additionalTurnout1", additionalTurnout1, additionalTurnout1FeebackReversed, false);    // NOI18N
079        addTurnoutMonitored(userIdentifier, "additionalTurnout2", additionalTurnout2, additionalTurnout2FeebackReversed, false);    // NOI18N
080        addTurnoutMonitored(userIdentifier, "additionalTurnout3", additionalTurnout3, additionalTurnout3FeebackReversed, false);    // NOI18N
081        updateDispatcherSensorIndicator(turnoutLocksEnabledAtStartup);
082        _mTurnoutsMonitoredPropertyChangeListener = (PropertyChangeEvent e) -> { handleTurnoutChange(e); };
083        for (NBHTurnout tempTurnout : _mTurnoutsMonitored) {
084            if (tempTurnout.getKnownState() == Turnout.UNKNOWN) {
085                tempTurnout.setCommandedState(_mCommandedState);    // MUST be done before "addPropertyChangeListener":
086            }
087            tempTurnout.addPropertyChangeListener(_mTurnoutsMonitoredPropertyChangeListener);
088        }
089    }
090
091    public void removeAllListeners() {
092        for (NBHTurnout tempTurnout : _mTurnoutsMonitored) {
093            tempTurnout.removePropertyChangeListener(_mTurnoutsMonitoredPropertyChangeListener);
094        }
095    }
096
097    public NBHSensor getDispatcherSensorLockToggle() { return _mDispatcherSensorLockToggle; }
098
099    private void addTurnoutMonitored(String userIdentifier, String parameter, NBHTurnout actualTurnout, boolean FeedbackDifferent, boolean required) {
100        boolean actualTurnoutPresent = actualTurnout.valid();
101        if (required && !actualTurnoutPresent) {
102            (new CTCException("TurnoutLock", userIdentifier, parameter, Bundle.getMessage("RequiredTurnoutMissing"))).logError();   // NOI18N
103            return;
104        }
105        if (actualTurnoutPresent) { // IF there is something there, try it:
106            if (actualTurnout.valid()) _mTurnoutsMonitored.add(actualTurnout);
107        }
108    }
109
110//  Was propertyChange:
111    private void handleTurnoutChange(PropertyChangeEvent e) {
112        if (e.getPropertyName().equals("KnownState")) { // NOI18N
113            if (_mLocked) {                                                 // Act on locked only!
114                NBHTurnout turnout = null;  // Not found.
115                for (int index = 0; index < _mTurnoutsMonitored.size(); index++) { // Find matching entry:
116                    if (e.getSource() == _mTurnoutsMonitored.get(index).getBean()) { // Matched:
117                        turnout = _mTurnoutsMonitored.get(index);
118                        break;
119                    }
120                }
121                if (turnout != null) { // Safety check:
122                    if (_mCommandedState != turnout.getKnownState()) {      // Someone in the field messed with it:
123                        turnoutSetCommandedState(turnout, _mCommandedState);       // Just directly restore it
124                    }
125                }
126            }
127        }
128    }
129
130/*
131External software calls this from initialization code in order to lock the turnout.  When this code starts
132up the lock status is UNLOCKED so that initialization code can do whatever to the turnout.
133This routine DOES NOT modify the state of the switch, ONLY the lock!
134*/
135    public void externalLockTurnout() {
136        _mDispatcherSensorLockToggle.setKnownState(Sensor.INACTIVE);
137        updateDispatcherSensorIndicator(true);
138    }
139
140//  Ditto above routine, except opposite:
141    public void externalUnlockTurnout() {
142        _mDispatcherSensorLockToggle.setKnownState(Sensor.ACTIVE);
143        updateDispatcherSensorIndicator(false);
144    }
145
146//  External software calls this (from CodeButtonHandler typically) to inform us of a valid code button push:
147    public void codeButtonPressed() {
148        boolean newLockedState = getNewLockedState();
149        if (newLockedState == _mLocked) return; // Nothing changed
150//  The PROTOTYPE would not do this: Since the dispatcher CANNOT control the state of the switch, and
151//  our operating crews (for example: "brains go dead going down the stairs") MAY forget to normalize the switch
152//  for the main (for instance), we FORCE the state of the switch(s) to a known state (hopefully for the main)
153        if (_mNoDispatcherControlOfSwitch && newLockedState == true) { // No dispatcher control of switch and LOCKING them, "normalize" the switch:
154            for (NBHTurnout turnout : _mTurnoutsMonitored) {
155                turnoutSetCommandedState(turnout, _m_ndcos_WhenLockedSwitchState);     // Make it so.
156            }
157        }
158        updateDispatcherSensorIndicator(newLockedState);
159    }
160
161// External software calls this (from CodeButtonHandler typically) to tell us of the new state of the turnout:
162    public void dispatcherCommandedState(int commandedState) {
163        if (commandedState == CTCConstants.SWITCHNORMAL) _mCommandedState = Turnout.CLOSED; else _mCommandedState = Turnout.THROWN;
164    }
165
166    public boolean turnoutPresentlyLocked() { return _mLocked; }
167
168    public boolean getNewLockedState() {
169        return _mDispatcherSensorLockToggle.getKnownState() == Sensor.INACTIVE;
170    }
171
172    public boolean tryingToChangeLockStatus() { return getNewLockedState() != _mLocked; }
173
174    private void turnoutSetCommandedState(NBHTurnout turnout, int state) {
175        _mCommandedState = state;
176        turnout.setCommandedState(state);
177    }
178
179    private void updateDispatcherSensorIndicator(boolean newLockedState) {
180        _mLocked = newLockedState;
181        _mDispatcherSensorUnlockedIndicator.setKnownState(_mLocked ? Sensor.INACTIVE : Sensor.ACTIVE);
182    }
183}