001/*
002 * @author Gregory J. Bedlek Copyright (C) 2018, 2019
003 *
004 */
005package jmri.jmrit.ctc;
006
007import java.util.ArrayList;
008import java.util.HashSet;
009import jmri.Block;
010import jmri.NamedBeanHandle;
011import jmri.Sensor;
012import jmri.jmrit.ctc.ctcserialdata.CallOnData;
013import jmri.jmrit.ctc.ctcserialdata.OtherData;
014
015/*
016This module supports Call On functionality.
017
018SignalHeads: Clears the "held" bit and sets the signal to whatever the user
019specified.   CalledOnSensor can be "" if you want the Dispatcher to call on to
020an unoccupied block, or the real sensor for the block being called on to.
021
022SignalMasts: Clears the "held" bit, and set the permissive value in the called
023on block.  In addition, see the block comment above where this is done regarding
024the bug in JMRI we fix.  When the O.S. section becomes occupied, we clear the
025permissive value in the called on block.
026
027Both:
028If the occupancy sensor is specified and that sensor is INACTIVE (indicating
029nothing in block), then the call on is IGNORED.
030Resets the "callOnToggleSensor" to INACTIVE, thereby turning off the
031Call On request by the dispatcher.
032
033Issues with a computer vs. a real CTC machine:
034
035If you have a call on button AND you set it to momentary and you have a
036computer monitor (vs. a physical dispatchers panel):
037It is IMPOSSIBLE to press both the Call on button AND the code button at the
038same time.  You could make the call on button NON momentary, but from my experiment
039(please perform you own), the physical indication of the button pressed is not
040good, and if you press it again, it toggles off which is not what you want.
041I suggest using a toggle switch.  This object when configured for using it
042will "reset" the toggle to off once the code button has been pressed
043thus providing a better indication that it occurred.  In the future, if you
044substitute a push button and forget to set the "momentary" attribute, this
045routine will still reset it for the next use.
046
047Rules from http://www.ctcparts.com/about.htm
048
049"An important note though for programming logic is that the interlocking limits
050must be clear and all power switches within the interlocking limits aligned
051appropriately for the back to train route for this feature to activate."
052
053I was either told or read somewhere that the block being called on to MUST be
054occupied for the call on to work, otherwise the CODE BUTTON PRESS is IGNORE!
055
056By the way, there is NO way to do flashing any color with a semaphore!
057You should probably use "YELLOW" in that case!
058
059*/
060public class CallOn {
061    private static class GroupingData {
062        public final NBHSignal _mSignal;         // Signal
063        public final int _mSignalHeadFaces;      // Which way above faces.
064        public final int _mCallOnAspect;         // What it should be set to if CallOn sucessful.
065        public final NBHSensor _mCalledOnExternalSensor;
066        public final NamedBeanHandle<Block> _mNamedBeanHandleBlock;
067        public final SwitchIndicatorsRoute _mRoute;
068//  NOTE: When calling this constructor, ALL values MUST BE VALID!  NO check is done!
069        public GroupingData(NBHSignal signal, String signalHeadFaces, int callOnAspect, NBHSensor calledOnExternalSensor, NamedBeanHandle<Block> namedBeanHandleBlock, SwitchIndicatorsRoute route) {
070            _mSignal = signal;
071            _mSignalHeadFaces = signalHeadFaces.equals(Bundle.getMessage("InfoDlgCOLeftTraffic")) ? CTCConstants.LEFTTRAFFIC : CTCConstants.RIGHTTRAFFIC;   // NOI18N
072            _mCallOnAspect = callOnAspect;
073            _mCalledOnExternalSensor = calledOnExternalSensor;
074            _mNamedBeanHandleBlock = namedBeanHandleBlock;
075            _mRoute = route;
076        }
077    }
078
079    private final LockedRoutesManager _mLockedRoutesManager;
080    private final boolean _mSignalHeadSelected;
081    private final NBHSensor _mCallOnToggleSensor;
082    private final ArrayList<GroupingData> _mGroupingDataArrayList = new ArrayList<>();
083
084    public CallOn(LockedRoutesManager lockedRoutesManager, String userIdentifier, NBHSensor callOnToggleSensor, ArrayList<CallOnData> groupingsList, OtherData.SIGNAL_SYSTEM_TYPE signalSystemType) {
085        _mLockedRoutesManager = lockedRoutesManager;
086        _mSignalHeadSelected = (signalSystemType == OtherData.SIGNAL_SYSTEM_TYPE.SIGNALHEAD);
087        _mCallOnToggleSensor = callOnToggleSensor;
088        for (CallOnData callOnData : groupingsList) {
089            try {
090                NBHSignal signal = callOnData._mExternalSignal;
091                String trafficDirection = callOnData._mSignalFacingDirection;
092                if (!trafficDirection.equals(Bundle.getMessage("InfoDlgCOLeftTraffic")) && !trafficDirection.equals(Bundle.getMessage("InfoDlgCORightTraffic"))) {  // NOI18N
093                    throw new CTCException("CallOn", userIdentifier, "groupingString", callOnData.toString() + " not " + Bundle.getMessage("InfoDlgCOLeftTraffic") + " or " + Bundle.getMessage("InfoDlgCORightTraffic") + ".");   // NOI18N
094                }
095                SwitchIndicatorsRoute route = new SwitchIndicatorsRoute(callOnData._mSwitchIndicators);
096                if (_mSignalHeadSelected) {
097//  Technically, I'd have liked to call this only once, but in reality, each signalhead could have a different value list:
098                    String[] validStateNames = signal.getValidStateNames(); // TODO consider using getValidStateKeys() to skip localisation issue
099                    int validStateNamesIndex = arrayFind(validStateNames, convertFromForeignLanguageColor(callOnData._mSignalAspectToDisplay));
100                    // TODO use non-localized validStateNKeys instead of localized validStateNames
101                    if (validStateNamesIndex == -1) { // Not found:
102                        throw new CTCException("CallOn", userIdentifier, "groupingString", callOnData.toString() + " " + Bundle.getMessage("CallOnNotValidAspect"));   // NOI18N
103                    }
104                    NBHSensor calledOnExternalSensor = callOnData._mCalledOnExternalSensor;
105                    int[] correspondingValidStates = signal.getValidStates();   // I ASSUME it's a correlated 1 for 1 with "getValidStateNames", via tests it seems to be.
106                    _mGroupingDataArrayList.add(new GroupingData(signal, trafficDirection, correspondingValidStates[validStateNamesIndex], calledOnExternalSensor, null, route));
107                } else {
108                    NamedBeanHandle<Block> externalBlock = callOnData._mExternalBlock;
109                    _mGroupingDataArrayList.add(new GroupingData(signal, trafficDirection, 0, null, externalBlock, route));
110                }
111            } catch (CTCException e) { e.logError(); return; }
112        }
113        resetToggle();
114    }
115
116    public void removeAllListeners() {}   // None done.
117
118    public void resetToggle() {
119        _mCallOnToggleSensor.setKnownState(Sensor.INACTIVE);
120    }
121
122/*  Call On requested.  CodeButtonHandler has determined from it's "limited"
123    viewpoint that it is OK to attempt the call on.  This routine determines
124    if it is fully valid to allow it at this time.
125
126NOTE:
127    We "fake out" the caller: we return "true" AS IF we actually did the
128    call on.  Why?  So higher level code in CodeButtonHandler DOES NOT attempt
129    to try normal signal direction lever handling!  When the dispatcher requests
130    call on, normal signal direction lever handling should be bypassed.  If we
131    didn't, then when the dispatcher requested call on to an unoccupied block,
132    the signals would go to yellow/green instead of staying at signals normal
133    (all stop) which would be the result of the call on only, which is what we
134    want.  After all, that's what the dispatcher asked for explicitly!
135*/
136    public TrafficLockingInfo codeButtonPressed(HashSet<Sensor> sensors,
137                                                String userIdentifier,
138                                                SignalDirectionIndicatorsInterface signalDirectionIndicatorsObject,
139                                                int signalDirectionLever) {
140        if (_mCallOnToggleSensor.getKnownState() == Sensor.INACTIVE) return new TrafficLockingInfo(false);    // Dispatcher didn't want it at this time!
141        if (signalDirectionLever != CTCConstants.LEFTTRAFFIC && signalDirectionLever != CTCConstants.RIGHTTRAFFIC) return new TrafficLockingInfo(false); // Doesn't make sense, don't do anything
142
143        GroupingData foundGroupingData = null;
144
145        int ruleNumber = 0;
146        for (GroupingData groupingData : _mGroupingDataArrayList) {
147            ruleNumber++;
148            if (groupingData._mSignalHeadFaces == signalDirectionLever) {
149                if (groupingData._mRoute.isRouteSelected()) {
150                    foundGroupingData = groupingData;
151                    break;
152                }
153            }
154        }
155//  From NOW ON, the "returnValue" status will be true:
156        TrafficLockingInfo returnValue = new TrafficLockingInfo(true);
157        if (foundGroupingData == null) return returnValue;    // Has to be active, pretend we did it, but we didn't!
158
159        if (_mSignalHeadSelected) {
160            if (Sensor.ACTIVE != foundGroupingData._mCalledOnExternalSensor.getKnownState()) return returnValue;    // Has to be active EXACTLY, pretend we did it, but we didn't!
161            if (foundGroupingData._mCalledOnExternalSensor.valid()) {
162                sensors.add(foundGroupingData._mCalledOnExternalSensor.getBean());
163            }
164//  Check to see if the route specified is free:
165//  The route is the O.S. section that called us, along with the called on occupancy sensor:
166            returnValue._mLockedRoute = _mLockedRoutesManager.checkRouteAndAllocateIfAvailable(sensors, userIdentifier, "Rule #" + ruleNumber, signalDirectionLever == CTCConstants.RIGHTTRAFFIC);
167            if (returnValue._mLockedRoute == null) return returnValue;         // Not available, fake out
168
169            foundGroupingData._mSignal.setHeld(false);    // Original order in .py code
170            foundGroupingData._mSignal.setAppearance(foundGroupingData._mCallOnAspect);
171        } else {    // Signal mast
172//  We get this EVERY time (we don't cache this in "foundGroupingData._mCalledOnExternalSensor") because this property of Block
173//  can be changed DYNAMICALLY at runtime via the Block Editor:
174            NBHSensor sensor = new NBHSensor(foundGroupingData._mNamedBeanHandleBlock.getBean().getNamedSensor());
175            if (Sensor.ACTIVE != sensor.getKnownState()) return returnValue;       // Has to be active EXACTLY, pretend we did it, but we didn't!
176            if (sensor.valid()) {
177                sensors.add(sensor.getBean());
178            }
179
180//  Check to see if the route specified is free:
181//  The route is the O.S. section that called us, along with the called on occupancy sensor:
182            returnValue._mLockedRoute = _mLockedRoutesManager.checkRouteAndAllocateIfAvailable(sensors, userIdentifier, "Rule #" + ruleNumber, signalDirectionLever == CTCConstants.RIGHTTRAFFIC);
183            if (returnValue._mLockedRoute == null) return returnValue;         // Not available, fake out
184            foundGroupingData._mSignal.allowPermissiveSML();
185            foundGroupingData._mSignal.setHeld(false);
186        }
187
188        signalDirectionIndicatorsObject.setRequestedDirection(signalDirectionLever);
189// These two statements MUST be last thing in this order:
190        signalDirectionIndicatorsObject.setSignalDirectionIndicatorsToOUTOFCORRESPONDENCE();
191        signalDirectionIndicatorsObject.startCodingTime();
192        return returnValue;
193    }
194
195    static private int arrayFind(String[] array, String aString) {
196        for (int index = 0; index < array.length; index++) {
197            if (aString.equals(array[index])) return index;
198        }
199        return -1;
200    }
201
202//  When we went to foreign language support, I had to convert to English here, so that these lines worked above:
203//  String[] validStateNames = signal.getValidStateNames(); // use getValidStateKeys instead?
204//  int validStateNamesIndex = arrayFind(validStateNames, convertFromForeignLanguageColor(callOnEntry._mSignalAspectToDisplay));
205//
206//  I SUSPECT (not verified) that "signal.getValidStateNames()" ALWAYS returns English no matter what language is selected.
207//  If I AM WRONG, then this routine can be removed, and the call to it removed:
208    private String convertFromForeignLanguageColor(String foreignLanguageColor) {
209        String color = "Red"; // should NEVER be used directly, but if programmers screw up, default to some "sane" value.
210        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateDark"))) color = "Dark";     // NOI18N
211        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateRed"))) color = "Red";       // NOI18N
212        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateYellow"))) color = "Yellow"; // NOI18N
213        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateGreen"))) color = "Green";   // NOI18N
214        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingRed"))) color = "Flashing Red";       // NOI18N
215        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingYellow"))) color = "Flashing Yellow"; // NOI18N
216        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingGreen"))) color = "Flashing Green";   // NOI18N
217        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateLunar"))) color = "Lunar";                    // NOI18N
218        if (foreignLanguageColor.equals(Bundle.getMessage("SignalHeadStateFlashingLunar"))) color = "Flashing Lunar";   // NOI18N
219        return color;
220    }
221
222}