001package jmri.jmrit.ctc;
002import java.beans.PropertyChangeEvent;
003import java.beans.PropertyChangeListener;
004import java.util.HashSet;
005import jmri.*;
006import org.slf4j.LoggerFactory;
007
008/**
009 * This is the "master" class that handles everything when a code button is
010 * pressed.  As such, it has a LOT of external data passed into it's constructor,
011 * and operates and modifies all objects it contains on a dynamic basis both
012 * when the button is pressed, and when external events happen that affect this
013 * object.
014 * <p>
015 * Notes:
016 * <p>
017 * Changing both signal direction to non signals normal and switch direction at the same time "is allowed".
018 * Lock/Unlock is the LOWEST priority!  Call on is the HIGHEST priority.
019 * <p>
020 * As of V1.04 of the CTC system, preconditioning (a.k.a. stacking) is supported.  It is enabled
021 * by setting the internal sensor (automatically created) "IS:PRECONDITIONING_ENABLED" to active.
022 * Any other value inactivates this feature.  For example, the user can create a toggle
023 * switch to activate / inactivate it.
024 *
025 * @author Gregory J. Bedlek Copyright (C) 2018, 2019, 2020
026 */
027public class CodeButtonHandler {
028    private final boolean _mTurnoutLockingOnlyEnabled;
029    private final LockedRoutesManager _mLockedRoutesManager;
030    private final String _mUserIdentifier;
031    private final int _mUniqueID;
032    private final NBHSensor _mCodeButtonInternalSensor;
033    private final PropertyChangeListener _mCodeButtonInternalSensorPropertyChangeListener;
034    private final NBHSensor _mOSSectionOccupiedExternalSensor;
035    private final NBHSensor _mOSSectionOccupiedExternalSensor2;
036    private final PropertyChangeListener _mOSSectionOccupiedExternalSensorPropertyChangeListener;
037    private final SignalDirectionIndicatorsInterface _mSignalDirectionIndicators;
038    private final SignalDirectionLever _mSignalDirectionLever;
039    private final SwitchDirectionIndicators _mSwitchDirectionIndicators;
040    private final SwitchDirectionLever _mSwitchDirectionLever;
041    private final Fleeting _mFleeting;
042    private final CallOn _mCallOn;
043    private final TrafficLocking _mTrafficLocking;
044    private final TurnoutLock _mTurnoutLock;
045    private final IndicationLockingSignals _mIndicationLockingSignals;
046    private final CodeButtonSimulator _mCodeButtonSimulator;
047    private LockedRoute _mLockedRoute = null;
048
049    private static final Sensor _mPreconditioningEnabledSensor = initializePreconditioningEnabledSensor();
050    private static class PreconditioningData {
051        public boolean  _mCodeButtonPressed = false;    // If false, values in these don't matter:
052        public int      _mSignalDirectionLeverWas = CTCConstants.OUTOFCORRESPONDENCE;   // Safety:
053        public int      _mSwitchDirectionLeverWas = CTCConstants.OUTOFCORRESPONDENCE;
054    }
055
056    private static Sensor initializePreconditioningEnabledSensor() {
057        Sensor returnValue = InstanceManager.sensorManagerInstance().newSensor("IS:PRECONDITIONING_ENABLED", null); // NOI18N
058        int knownState = returnValue.getKnownState();
059        if (Sensor.ACTIVE != knownState && Sensor.INACTIVE != knownState) {
060            try {returnValue.setKnownState(Sensor.INACTIVE); } catch (JmriException ex) {
061                LoggerFactory.getLogger(CodeButtonHandler.class).debug("Sensor problem, preconditioning won't work.");          // NOI18N
062            }
063        }
064        return returnValue;
065    }
066    private PreconditioningData _mPreconditioningData = new PreconditioningData();
067
068    public CodeButtonHandler(   boolean turnoutLockingOnlyEnabled,                              // If this is NOT an O.S. section, but only a turnout lock, then this is true.
069                                LockedRoutesManager lockedRoutesManager,
070                                String userIdentifier,
071                                int uniqueID,
072                                NBHSensor codeButtonInternalSensor,                             // Required
073                                int codeButtonDelayInMilliseconds,                              // If 0, REAL code button, if > 0, tower operations (simulated code button).
074                                NBHSensor osSectionOccupiedExternalSensor,                      // Required, if ACTIVE prevents turnout, lock or call on from occuring.
075                                NBHSensor osSectionOccupiedExternalSensor2,                     // Optional, if ACTIVE prevents turnout, lock or call on from occuring.
076                                SignalDirectionIndicatorsInterface signalDirectionIndicators,   // Required
077                                SignalDirectionLever signalDirectionLever,
078                                SwitchDirectionIndicators switchDirectionIndicators,
079                                SwitchDirectionLever switchDirectionLever,
080                                Fleeting fleeting,                                              // If null, then ALWAYS fleeting!
081                                CallOn callOn,
082                                TrafficLocking trafficLocking,
083                                TurnoutLock turnoutLock,
084                                IndicationLockingSignals indicationLockingSignals) {            // Needed for check of adjacent OS Section(s), and optionally turnoutLock.
085        signalDirectionIndicators.setCodeButtonHandler(this);
086        _mTurnoutLockingOnlyEnabled = turnoutLockingOnlyEnabled;
087        _mLockedRoutesManager = lockedRoutesManager;
088        _mUserIdentifier = userIdentifier;
089        _mUniqueID = uniqueID;
090        _mSignalDirectionIndicators = signalDirectionIndicators;
091        _mSignalDirectionLever = signalDirectionLever;
092        _mSwitchDirectionIndicators = switchDirectionIndicators;
093        _mSwitchDirectionLever = switchDirectionLever;
094        _mFleeting = fleeting;
095        _mCallOn = callOn;
096        _mTrafficLocking = trafficLocking;
097        _mTurnoutLock = turnoutLock;
098        _mIndicationLockingSignals = indicationLockingSignals;
099        _mCodeButtonInternalSensor = codeButtonInternalSensor;
100        _mCodeButtonInternalSensor.setKnownState(Sensor.INACTIVE);
101        _mCodeButtonInternalSensorPropertyChangeListener = (PropertyChangeEvent e) -> { codeButtonStateChange(e); };
102        _mCodeButtonInternalSensor.addPropertyChangeListener(_mCodeButtonInternalSensorPropertyChangeListener);
103
104        _mOSSectionOccupiedExternalSensorPropertyChangeListener = (PropertyChangeEvent e) -> { osSectionPropertyChangeEvent(e); };
105        _mOSSectionOccupiedExternalSensor = osSectionOccupiedExternalSensor;
106        _mOSSectionOccupiedExternalSensor.addPropertyChangeListener(_mOSSectionOccupiedExternalSensorPropertyChangeListener);
107
108// NO property change for this, only used for turnout locking:
109        _mOSSectionOccupiedExternalSensor2 = osSectionOccupiedExternalSensor2;
110
111        if (codeButtonDelayInMilliseconds > 0) { // SIMULATED code button:
112            _mCodeButtonSimulator = new CodeButtonSimulator(codeButtonDelayInMilliseconds,
113                                                            _mCodeButtonInternalSensor,
114                                                            _mSwitchDirectionLever,
115                                                            _mSignalDirectionLever,
116                                                            _mTurnoutLock);
117        } else {
118            _mCodeButtonSimulator = null;
119        }
120    }
121
122    /**
123     * This routine SHOULD ONLY be called by CTCMain when the CTC system is shutdown
124     * in order to clean up all resources prior to a restart.  Nothing else should
125     * call this.
126     */
127    public void removeAllListeners() {
128//  Remove our registered listeners first:
129        _mCodeButtonInternalSensor.removePropertyChangeListener(_mCodeButtonInternalSensorPropertyChangeListener);
130        _mOSSectionOccupiedExternalSensor.removePropertyChangeListener(_mOSSectionOccupiedExternalSensorPropertyChangeListener);
131//  Give each object a chance to remove theirs also:
132        if (_mSignalDirectionIndicators != null) _mSignalDirectionIndicators.removeAllListeners();
133        if (_mSignalDirectionLever != null) _mSignalDirectionLever.removeAllListeners();
134        if (_mSwitchDirectionIndicators != null) _mSwitchDirectionIndicators.removeAllListeners();
135        if (_mSwitchDirectionLever != null) _mSwitchDirectionLever.removeAllListeners();
136        if (_mFleeting != null) _mFleeting.removeAllListeners();
137        if (_mCallOn != null) _mCallOn.removeAllListeners();
138        if (_mTrafficLocking != null) _mTrafficLocking.removeAllListeners();
139        if (_mTurnoutLock != null) _mTurnoutLock.removeAllListeners();
140        if (_mIndicationLockingSignals != null) _mIndicationLockingSignals.removeAllListeners();
141        if (_mCodeButtonSimulator != null) _mCodeButtonSimulator.removeAllListeners();
142    }
143
144    /**
145     * SignalDirectionIndicators calls us here when time locking is done.
146     */
147    public void cancelLockedRoute() {
148        _mLockedRoutesManager.cancelLockedRoute(_mLockedRoute);     // checks passed parameter for null for us
149        _mLockedRoute = null;       // Not valid anymore.
150    }
151
152    public boolean uniqueIDMatches(int uniqueID) { return _mUniqueID == uniqueID; }
153    public NBHSensor getOSSectionOccupiedExternalSensor() { return _mOSSectionOccupiedExternalSensor; }
154
155    private void osSectionPropertyChangeEvent(PropertyChangeEvent e) {
156        if (isPrimaryOSSectionOccupied()) { // MUST ALWAYS process PRIMARY OS occupied state change to ACTIVE (It's the only one that comes here anyways!)
157            if (_mFleeting != null && !_mFleeting.isFleetingEnabled()) { // Impliment "stick" here:
158                _mSignalDirectionIndicators.forceAllSignalsToHeld();
159            }
160            _mSignalDirectionIndicators.osSectionBecameOccupied();
161        }
162        else { // Process pre-conditioning if available:
163            if (_mPreconditioningData._mCodeButtonPressed) {
164                doCodeButtonPress();
165                _mPreconditioningData._mCodeButtonPressed = false;
166            }
167        }
168    }
169
170    public void externalLockTurnout() {
171        if (_mTurnoutLock != null) _mTurnoutLock.externalLockTurnout();
172    }
173
174    private void codeButtonStateChange(PropertyChangeEvent e) {
175        if (e.getPropertyName().equals("KnownState") && (int)e.getNewValue() == Sensor.ACTIVE) {
176//  NOTE: If the primary O.S. section is occupied, you CANT DO ANYTHING via a CTC machine, except:
177//  Preconditioning: IF the O.S. section is occupied, then it is a pre-conditioning request:
178            if (isPrimaryOSSectionOccupied()) {
179                if (Sensor.ACTIVE == _mPreconditioningEnabledSensor.getKnownState()) {  // ONLY if turned on:
180                    _mPreconditioningData._mSignalDirectionLeverWas = getCurrentSignalDirectionLever(false);
181                    _mPreconditioningData._mSwitchDirectionLeverWas = getSwitchDirectionLeverRequestedState(false);
182                    _mPreconditioningData._mCodeButtonPressed = true;   // Do this LAST so that the above variables are stable in this object,
183                                                                        // in case there is a multi-threading issue (yea, lock it would be better,
184                                                                        // but this is good enough for now!)
185                }
186            }
187            doCodeButtonPress();
188        }
189    }
190
191    private void doCodeButtonPress() {
192        if (_mSignalDirectionIndicators.isRunningTime()) return;    // If we are running time, IGNORE all requests from the user:
193        possiblyAllowLockChange();                              // MUST unlock first, otherwise if dispatcher wanted to unlock and change switch state, it wouldn't!
194        possiblyAllowTurnoutChange();                           // Change turnout
195//  IF the call on was accepted, then we DON'T attempt to change the signals to a more favorable
196//  aspect here.  Additionally see the comments above CallOn.java/"codeButtonPressed" for an explanation
197//  of a "fake out" that happens in that routine, and it's effect on this code here:
198        if (!possiblyAllowCallOn()) {                           // NO call on occured or was allowed or requested:
199            possiblyAllowSignalDirectionChange();               // Slave to it!
200        }
201    }
202
203// Returns true if call on was actually done, else false
204    private boolean possiblyAllowCallOn() {
205        boolean returnStatus = false;
206        if (allowCallOnChange()) {
207            HashSet<Sensor> sensors = new HashSet<>();                  // Initial O.S. section sensor(s):
208            sensors.add(_mOSSectionOccupiedExternalSensor.getBean());   // Always.
209//  If there is a switch direction indicator, and it is reversed, then add the other sensor if valid here:
210            if (_mSwitchDirectionIndicators != null && _mSwitchDirectionIndicators.getLastIndicatorState() == CTCConstants.SWITCHREVERSED) {
211                if (_mOSSectionOccupiedExternalSensor2.valid()) sensors.add(_mOSSectionOccupiedExternalSensor2.getBean());
212            }
213//  NOTE: We DO NOT support preconditioning of call on, ergo false passed to "getCurrentSignalDirectionLever"
214            TrafficLockingInfo trafficLockingInfo = _mCallOn.codeButtonPressed(sensors, _mUserIdentifier, _mSignalDirectionIndicators, getCurrentSignalDirectionLever(false));
215            if (trafficLockingInfo._mLockedRoute != null) { // Was allocated:
216                _mLockedRoute = trafficLockingInfo._mLockedRoute;
217            }
218            returnStatus = trafficLockingInfo._mReturnStatus;
219        }
220        if (_mCallOn != null) _mCallOn.resetToggle();
221        return returnStatus;
222    }
223
224/*
225Rules from http://www.ctcparts.com/about.htm
226    "An important note though for programming logic is that the interlocking limits
227must be clear and all power switches within the interlocking limits aligned
228appropriately for the back to train route for this feature to activate."
229*/
230    private boolean allowCallOnChange() {
231// Safety checks:
232        if (_mCallOn == null) return false;
233// Rules:
234        if (isPrimaryOSSectionOccupied()) return false;
235        if (_mSignalDirectionIndicators.isRunningTime()) return false;
236        if (_mSignalDirectionIndicators.getSignalsInTheFieldDirection() != CTCConstants.SIGNALSNORMAL) return false;
237        if (!areOSSensorsAvailableInRoutes()) return false;
238        return true;
239    }
240
241//  If it doesn't exist, this returns OUTOFCORRESPONDENCE, else return it's present state:
242//  NOTE: IF a preconditioned input was available, it OVERRIDES actual Signal Direction Lever (which is ignored in this case).
243    private int getCurrentSignalDirectionLever(boolean allowMergeInPreconditioning) {
244        if (_mSignalDirectionLever == null) return CTCConstants.OUTOFCORRESPONDENCE;
245        if (allowMergeInPreconditioning && _mPreconditioningData._mCodeButtonPressed) { // We can check and it is available:
246            if (_mPreconditioningData._mSignalDirectionLeverWas == CTCConstants.LEFTTRAFFIC
247            || _mPreconditioningData._mSignalDirectionLeverWas == CTCConstants.RIGHTTRAFFIC) { // Was valid:
248                return _mPreconditioningData._mSignalDirectionLeverWas;
249            }
250        }
251        return _mSignalDirectionLever.getPresentSignalDirectionLeverState();
252    }
253
254    private void possiblyAllowTurnoutChange() {
255        if (allowTurnoutChange()) {
256            int requestedState = getSwitchDirectionLeverRequestedState(true);
257            notifyTurnoutLockObjectOfNewAlignment(requestedState);          // Tell lock object this is new alignment
258            if (_mSwitchDirectionIndicators != null) { // Safety:
259                _mSwitchDirectionIndicators.codeButtonPressed(requestedState);  // Also sends commmands to move the points
260            }
261        }
262    }
263
264    private boolean allowTurnoutChange() {
265// Safety checks:
266// Rules:
267        if (!_mSignalDirectionIndicators.signalsNormal()) return false;
268        if (routeClearedAcross()) return false;               // Something was cleared thru, NO CHANGE
269        if (isEitherOSSectionOccupied()) return false;
270// 6/28/16: If the switch direction indicators are presently "OUTOFCORRESPONDENCE", IGNORE request, as we are presently working on a change:
271        if (!switchDirectionIndicatorsInCorrespondence()) return false;
272        if (!turnoutPresentlyLocked()) return false;
273        if (!areOSSensorsAvailableInRoutes()) return false;
274        return true;
275    }
276
277    private void notifyTurnoutLockObjectOfNewAlignment(int requestedState) {
278        if (_mTurnoutLock != null) _mTurnoutLock.dispatcherCommandedState(requestedState);
279    }
280
281//  If it doesn't exist, this returns OUTOFCORRESPONDENCE, else return it's present state:
282//  NOTE: IF a preconditioned input was available, it OVERRIDES actual Switch Direction Lever (which is ignored in this case).
283    private int getSwitchDirectionLeverRequestedState(boolean allowMergeInPreconditioning) {
284        if (_mSwitchDirectionLever == null) return CTCConstants.OUTOFCORRESPONDENCE;
285        if (allowMergeInPreconditioning && _mPreconditioningData._mCodeButtonPressed) { // We can check and it is available:
286            if (_mPreconditioningData._mSwitchDirectionLeverWas == CTCConstants.SWITCHNORMAL
287            || _mPreconditioningData._mSwitchDirectionLeverWas == CTCConstants.SWITCHREVERSED) { // Was valid:
288                return _mPreconditioningData._mSwitchDirectionLeverWas;
289            }
290        }
291        return _mSwitchDirectionLever.getPresentState();
292    }
293
294// If it doesn't exist, this returns true.
295    private boolean switchDirectionIndicatorsInCorrespondence() {
296        if (_mSwitchDirectionIndicators != null) return _mSwitchDirectionIndicators.inCorrespondence();
297        return true;
298    }
299
300    private void possiblyAllowSignalDirectionChange() {
301        if (allowSignalDirectionChangePart1()) {
302            int presentSignalDirectionLever = getCurrentSignalDirectionLever(true);
303            int presentSignalDirectionIndicatorsDirection = _mSignalDirectionIndicators.getPresentDirection();  // Object always exists!
304            boolean requestedChangeInSignalDirection = (presentSignalDirectionLever != presentSignalDirectionIndicatorsDirection);
305// If Dispatcher is asking for a cleared signal direction:
306            if (presentSignalDirectionLever != CTCConstants.SIGNALSNORMAL) {
307                if (!requestedChangeInSignalDirection) return;  // If presentSignalDirectionLever is the same as the current state, DO NOTHING!
308            }
309// If user is trying to change direction, FORCE to "SIGNALSNORMAL" per Rick Moser response of 6/29/16:
310            if (presentSignalDirectionLever == CTCConstants.LEFTTRAFFIC && presentSignalDirectionIndicatorsDirection == CTCConstants.RIGHTTRAFFIC)
311                presentSignalDirectionLever = CTCConstants.SIGNALSNORMAL;
312            else if (presentSignalDirectionLever == CTCConstants.RIGHTTRAFFIC && presentSignalDirectionIndicatorsDirection == CTCConstants.LEFTTRAFFIC)
313                presentSignalDirectionLever = CTCConstants.SIGNALSNORMAL;
314
315            if (allowSignalDirectionChangePart2(presentSignalDirectionLever)) {
316// Tell SignalDirectionIndicators what the current requested state is:
317                _mSignalDirectionIndicators.setPresentSignalDirectionLever(presentSignalDirectionLever);
318                _mSignalDirectionIndicators.codeButtonPressed(presentSignalDirectionLever, requestedChangeInSignalDirection);
319            }
320        }
321    }
322
323    private boolean allowSignalDirectionChangePart1() {
324// Safety Checks:
325        if (_mSignalDirectionLever == null) return false;
326// Rules:
327// 6/28/16: If the signal direction indicators are presently "OUTOFCORRESPONDENCE", IGNORE request, as we are presently working on a change:
328        if (!_mSignalDirectionIndicators.inCorrespondence()) return false;
329        if (!turnoutPresentlyLocked()) return false;
330        return true;                                    // Allowed "so far".
331    }
332
333    private boolean allowSignalDirectionChangePart2(int presentSignalDirectionLever) {
334// Safety Checks: (none so far)
335// Rules:
336        if (presentSignalDirectionLever != CTCConstants.SIGNALSNORMAL) {
337// If asking for a route and these indicates an error (a conflict), DO NOTHING!
338            if (!trafficLockingValid(presentSignalDirectionLever)) return false;       // Do NOTHING at this time!
339        }
340        return true;                                    // Allowed
341    }
342
343    private boolean trafficLockingValid(int presentSignalDirectionLever) {
344// If asking for a route and it indicates an error (a conflict), DO NOTHING!
345        if (_mTrafficLocking != null) {
346            TrafficLockingInfo trafficLockingInfo = _mTrafficLocking.valid(presentSignalDirectionLever, _mFleeting.isFleetingEnabled());
347            _mLockedRoute = trafficLockingInfo._mLockedRoute;   // Can be null! This is the bread crumb trail when running time expires.
348            return trafficLockingInfo._mReturnStatus;
349        }
350        return true;        // Valid
351    }
352
353    private void possiblyAllowLockChange() {
354        if (allowLockChange()) _mTurnoutLock.codeButtonPressed();
355    }
356
357    private boolean allowLockChange() {
358// Safety checks:
359        if (_mTurnoutLock == null) return false;
360// Rules:
361// Degenerate case: If we ONLY have a lock toggle switch, code button and lock indicator then:
362// if these 3 are null and the provided signalDirectionIndocatorsObject is non functional, therefore ALWAYS allow it!
363//      if (_mSignalDirectionIndicators.isNonfunctionalObject() && _mSignalDirectionLever == null && _mSwitchDirectionIndicators == null && _mSwitchDirectionLever == null) return true;
364//  If this is a normal O.S. section, then if either is occupied, DO NOT allow unlock.
365//  If this is NOT an O.S. section, but only a lock, AND the dispatcher is trying
366//  to UNLOCK or LOCK this section, occupancy is not considered:
367        if (!_mTurnoutLockingOnlyEnabled) { // Normal O.S. section:
368            if (isEitherOSSectionOccupied()) return false;
369        }
370        if (!_mTurnoutLock.tryingToChangeLockStatus()) return false;
371        if (routeClearedAcross()) return false;
372        if (!_mSignalDirectionIndicators.signalsNormal()) return false;
373        if (!switchDirectionIndicatorsInCorrespondence()) return false;
374        if (!areOSSensorsAvailableInRoutes()) return false;
375        return true;
376    }
377
378    private boolean routeClearedAcross() {
379        if (_mIndicationLockingSignals != null) return _mIndicationLockingSignals.routeClearedAcross();
380        return false; // Default: Nothing to evaluate, nothing cleared thru!
381    }
382
383    private boolean turnoutPresentlyLocked() {
384        if (_mTurnoutLock == null) return true;     // Doesn't exist, assume locked so that anything can be done to it.
385        return _mTurnoutLock.turnoutPresentlyLocked();
386    }
387
388//  For "isEitherOSSectionOccupied" and "isPrimaryOSSectionOccupied" below,
389//  INCONSISTENT, UNKNOWN and OCCUPIED are all considered OCCUPIED(ACTIVE).
390    private boolean isEitherOSSectionOccupied() {
391        return _mOSSectionOccupiedExternalSensor.getKnownState() != Sensor.INACTIVE || _mOSSectionOccupiedExternalSensor2.getKnownState() != Sensor.INACTIVE;
392    }
393
394//  See "isEitherOSSectionOccupied" comment.
395    private boolean isPrimaryOSSectionOccupied() {
396        return _mOSSectionOccupiedExternalSensor.getKnownState() != Sensor.INACTIVE;
397    }
398
399    private boolean areOSSensorsAvailableInRoutes() {
400        HashSet<Sensor> sensors = new HashSet<>();
401        sensors.add(_mOSSectionOccupiedExternalSensor.getBean());
402        if (_mOSSectionOccupiedExternalSensor2.valid()) sensors.add(_mOSSectionOccupiedExternalSensor2.getBean());
403        return _mLockedRoutesManager.checkRoute(sensors, _mUserIdentifier, "Turnout Check");
404    }
405}