001/*
002 *  @author Gregory J. Bedlek Copyright (C) 2018, 2019
003 */
004package jmri.jmrit.ctc;
005
006import java.awt.event.ActionListener;
007import java.beans.PropertyChangeEvent;
008import java.beans.PropertyChangeListener;
009import java.util.ArrayList;
010import java.util.HashSet;
011import java.util.LinkedList;
012import javax.swing.Timer;
013import jmri.Sensor;
014import jmri.SignalAppearanceMap;
015import jmri.SignalHead;
016import jmri.implementation.AbstractSignalHead;
017import jmri.implementation.AbstractSignalMast;
018import jmri.jmrit.ctc.ctcserialdata.CodeButtonHandlerData;
019
020public final class SignalDirectionIndicators implements SignalDirectionIndicatorsInterface {
021    static final HashSet<NBHSignal> _mSignalsUsed = new HashSet<>();
022    public static void resetSignalsUsed() { _mSignalsUsed.clear(); }
023    private NBHSensor _mLeftSensor;
024    private NBHSensor _mNormalSensor;
025    private NBHSensor _mRightSensor;
026    private int _mPresentSignalDirectionLever = CTCConstants.SIGNALSNORMAL;             // Default
027    private final ArrayList<NBHSignal> _mSignalListLeftRight = new ArrayList<>();
028    private final ArrayList<NBHSignal> _mSignalListRightLeft = new ArrayList<>();
029    private Fleeting _mFleetingObject;
030    private final RequestedDirectionObserved _mRequestedDirectionObserver = new RequestedDirectionObserved();
031    private final Timer _mTimeLockingTimer;
032    private final ActionListener _mTimeLockingTimerActionListener;
033    private final Timer _mCodingTimeTimer;
034    private final ActionListener _mCodingTimeTimerActionListener;
035    private int _mPresentDirection;
036    private CodeButtonHandler _mCodeButtonHandler = null;
037    @Override
038    public void setCodeButtonHandler(CodeButtonHandler codeButtonHandler) { _mCodeButtonHandler = codeButtonHandler; }
039
040    private LinkedList<SignalHeadPropertyChangeListenerMaintainer> _mSignalHeadPropertyChangeListenerLinkedList = new LinkedList<>();
041//    @SuppressWarnings("LeakingThisInConstructor")   // NOI18N
042    private class SignalHeadPropertyChangeListenerMaintainer {
043        private final NBHSignal _mSignal;
044        private final PropertyChangeListener _mPropertyChangeListener = (PropertyChangeEvent e) -> { handleSignalChange(e); };
045        public SignalHeadPropertyChangeListenerMaintainer(NBHSignal signal) {
046            _mSignal = signal;
047            _mSignal.addPropertyChangeListener(_mPropertyChangeListener);
048            _mSignalHeadPropertyChangeListenerLinkedList.add(this); // "leaking this in constructor" is OK here, since this is the last thing we do.  And we are NOT multi-threaded when this happens.
049        }
050        public void removePropertyChangeListener() {
051            _mSignal.removePropertyChangeListener(_mPropertyChangeListener);
052        }
053    }
054
055/*  From: https://docs.oracle.com/javase/tutorial/collections/implementations/list.html
056    CopyOnWriteArrayList is a List implementation backed up by a copy-on-write array.
057    This implementation is similar in nature to CopyOnWriteArraySet. No synchronization
058    is necessary, even during iteration, and iterators are guaranteed never to throw
059    ConcurrentModificationException. This implementation is well suited to maintaining
060    event-handler lists, in which change is infrequent, and traversal is frequent and
061    potentially time-consuming.
062*/
063//  private final CopyOnWriteArrayList<TrafficDirection> _mTimeLockingChangeObservers = new CopyOnWriteArrayList<>();
064
065    public SignalDirectionIndicators(   String userIdentifier,
066                                        NBHSensor leftSensor,
067                                        NBHSensor normalSensor,
068                                        NBHSensor rightSensor,
069                                        int codingTimeInMilliseconds,
070                                        int timeLockingTimeInMilliseconds,
071                                        CodeButtonHandlerData.TRAFFIC_DIRECTION trafficDirection,
072                                        ArrayList<NBHSignal> signalListLeftRight,
073                                        ArrayList<NBHSignal> signalListRightLeft,
074                                        Fleeting fleetingObject) {
075
076// We need to give time to the ABS system to set signals.  See CALL to routine "allSignalsRedSetThemAllHeld", comments above that line:
077        if (codingTimeInMilliseconds < 100) codingTimeInMilliseconds = 100;
078        _mTimeLockingTimerActionListener = (ActionEvent) -> { timeLockingDone(); };
079        _mTimeLockingTimer = new Timer(codingTimeInMilliseconds + timeLockingTimeInMilliseconds, _mTimeLockingTimerActionListener);
080        _mTimeLockingTimer.setRepeats(false);
081        _mCodingTimeTimerActionListener = (ActionEvent) -> { codingTimeDone(); };
082        _mCodingTimeTimer = new Timer(codingTimeInMilliseconds, _mCodingTimeTimerActionListener);
083        _mCodingTimeTimer.setRepeats(false);
084        try {
085            _mLeftSensor = leftSensor;
086            _mNormalSensor = normalSensor;
087            _mRightSensor = rightSensor;
088//  Partially plagerized from GUI code:
089            boolean leftTrafficDirection = trafficDirection != CodeButtonHandlerData.TRAFFIC_DIRECTION.RIGHT;
090            boolean rightTrafficDirection = trafficDirection != CodeButtonHandlerData.TRAFFIC_DIRECTION.LEFT;
091
092            boolean entriesInLeftRightTrafficSignalsList = !signalListLeftRight.isEmpty();
093            boolean entriesInRightLeftTrafficSignalsList = !signalListRightLeft.isEmpty();
094
095            if (leftTrafficDirection && !entriesInRightLeftTrafficSignalsList) { throw new CTCException("SignalDirectionIndicators", userIdentifier, Bundle.getMessage("SignalDirectionIndicatorsInvalidCombination"), Bundle.getMessage("SignalDirectionIndicatorsError2")); }     // NOI18N
096            if (rightTrafficDirection && !entriesInLeftRightTrafficSignalsList) { throw new CTCException("SignalDirectionIndicators", userIdentifier, Bundle.getMessage("SignalDirectionIndicatorsInvalidCombination"), Bundle.getMessage("SignalDirectionIndicatorsError3")); }    // NOI18N
097            if (!leftTrafficDirection && entriesInRightLeftTrafficSignalsList) { throw new CTCException("SignalDirectionIndicators", userIdentifier, Bundle.getMessage("SignalDirectionIndicatorsInvalidCombination"), Bundle.getMessage("SignalDirectionIndicatorsError4")); }      // NOI18N
098            if (!rightTrafficDirection && entriesInLeftRightTrafficSignalsList) { throw new CTCException("SignalDirectionIndicators", userIdentifier, Bundle.getMessage("SignalDirectionIndicatorsInvalidCombination"), Bundle.getMessage("SignalDirectionIndicatorsError5")); }    // NOI18N
099
100            for (NBHSignal signal : signalListLeftRight) {
101                new SignalHeadPropertyChangeListenerMaintainer(signal); // Lazy, constructor does EVERYTHING and leaves a bread crumb trail to this object.
102                _mSignalListLeftRight.add(signal);
103                addSignal(userIdentifier, signal);
104            }
105
106            for (NBHSignal signal : signalListRightLeft) {
107                new SignalHeadPropertyChangeListenerMaintainer(signal); // Lazy, constructor does EVERYTHING and leaves a bread crumb trail to this object.
108                _mSignalListRightLeft.add(signal);
109                addSignal(userIdentifier, signal);
110            }
111
112            _mFleetingObject = fleetingObject;
113            setSignalDirectionIndicatorsToDirection(CTCConstants.SIGNALSNORMAL);
114            forceAllSignalsToHeld();
115          }
116          catch (CTCException e) { e.logError(); return; }
117    }
118
119    @Override
120    public void removeAllListeners() {
121        _mCodingTimeTimer.stop();       // Safety:
122        _mCodingTimeTimer.removeActionListener(_mCodingTimeTimerActionListener);
123        _mTimeLockingTimer.stop();
124        _mTimeLockingTimer.removeActionListener(_mTimeLockingTimerActionListener);
125        _mSignalHeadPropertyChangeListenerLinkedList.forEach((signalHeadPropertyChangeListenerMaintainer) -> {
126            signalHeadPropertyChangeListenerMaintainer.removePropertyChangeListener();
127        });
128    }
129
130    @Override
131    public boolean isNonfunctionalObject() { return false; }
132
133    @Override
134    public void setPresentSignalDirectionLever(int presentSignalDirectionLever) { _mPresentSignalDirectionLever = presentSignalDirectionLever; }
135
136    @Override
137    public boolean isRunningTime() { return _mTimeLockingTimer.isRunning(); }
138
139    @Override
140    public void osSectionBecameOccupied() {
141        _mCodingTimeTimer.stop();
142        _mTimeLockingTimer.stop();      // MUST be done before the next line:
143        possiblyUpdateSignalIndicationSensors();
144    }
145
146    @Override
147    public void codeButtonPressed(int requestedDirection, boolean requestedChangeInSignalDirection) {
148// Valid to process:
149        _mCodingTimeTimer.stop();
150        _mRequestedDirectionObserver.setRequestedDirection(requestedDirection);         // Superfluous since "setSignalsHeldto" does the same, but I'll leave it here
151        if (requestedDirection == CTCConstants.SIGNALSNORMAL) {     // Wants ALL STOP.
152            if (_mPresentDirection != CTCConstants.SIGNALSNORMAL) { // And is NOT all stop, run time:
153                _mTimeLockingTimer.start();
154                requestedChangeInSignalDirection = true;    // And override what is passed
155            }
156        }
157// ONLY start the coding timer IF we aren't running time.
158        if (!isRunningTime()) { startCodingTime(); }
159        if (requestedChangeInSignalDirection) setSignalDirectionIndicatorsToOUTOFCORRESPONDENCE();
160        setSignalsHeldTo(requestedDirection);
161    }
162
163    @Override
164    public void startCodingTime() {
165        _mCodingTimeTimer.start();
166    }
167
168    @Override
169    public boolean signalsNormal() {
170        return _mPresentDirection == CTCConstants.SIGNALSNORMAL;
171    }
172
173    @Override
174    public boolean signalsNormalOrOutOfCorrespondence() {
175        return _mPresentDirection == CTCConstants.SIGNALSNORMAL || _mPresentDirection == CTCConstants.OUTOFCORRESPONDENCE;
176    }
177
178    @Override
179    public int getPresentDirection() {
180        return _mPresentDirection;
181    }
182
183    @Override
184    public boolean inCorrespondence() {
185        return _mPresentDirection != CTCConstants.OUTOFCORRESPONDENCE;
186    }
187
188    @Override
189    public void forceAllSignalsToHeld() {
190        setSignalsHeldTo(CTCConstants.SIGNALSNORMAL);
191    }
192
193    @Override
194    public int getSignalsInTheFieldDirection() {
195        boolean LRCanGo = false;
196        boolean RLCanGo = false;
197        for (NBHSignal signal : _mSignalListLeftRight) {
198            if (!signal.isDanger()) { LRCanGo = true; break; }
199        }
200        for (NBHSignal signal : _mSignalListRightLeft) {
201            if (!signal.isDanger()) { RLCanGo = true; break; }
202        }
203        if (LRCanGo && RLCanGo) {
204            CTCException.logError(Bundle.getMessage("SignalDirectionIndicatorsError6"));    // NOI18N
205            setSignalDirectionIndicatorsToOUTOFCORRESPONDENCE();    // ooppss!
206            return CTCConstants.OUTOFCORRESPONDENCE;
207        }
208        if (LRCanGo) return CTCConstants.RIGHTTRAFFIC;
209        if (RLCanGo) return CTCConstants.LEFTTRAFFIC;
210        return CTCConstants.SIGNALSNORMAL;
211    }
212
213    @Override
214    public void setSignalDirectionIndicatorsToOUTOFCORRESPONDENCE() {
215        setSignalDirectionIndicatorsToDirection(CTCConstants.OUTOFCORRESPONDENCE);
216    }
217
218    @Override
219    public void setRequestedDirection(int direction) {
220        _mRequestedDirectionObserver.setRequestedDirection(direction);
221    }
222
223    private void addSignal(String userIdentifier, NBHSignal signal) throws CTCException {
224        if (!_mSignalsUsed.add(signal)) { throw new CTCException("SignalDirectionIndicators", userIdentifier, signal.getHandleName(), Bundle.getMessage("SignalDirectionIndicatorsDuplicateHomeSignal")); }    // NOI18N
225    }
226
227    private void setSignalsHeldTo(int direction) {
228        switch (direction) {
229            case CTCConstants.LEFTTRAFFIC:
230                setLRSignalsHeldTo(true);
231                setRLSignalsHeldTo(false);
232                break;
233            case CTCConstants.RIGHTTRAFFIC:
234                setLRSignalsHeldTo(false);
235                setRLSignalsHeldTo(true);
236                break;
237            default:    // Could be OUTOFCORRESPONDENCE or SIGNALSNORMAL:
238                setLRSignalsHeldTo(true);
239                setRLSignalsHeldTo(true);
240                break;
241        }
242        _mRequestedDirectionObserver.setRequestedDirection(direction);
243    }
244
245    private void setRLSignalsHeldTo(boolean held) { _mSignalListRightLeft.forEach((signal) -> {
246        signal.setHeld(held);
247        });
248}
249    private void setLRSignalsHeldTo(boolean held) { _mSignalListLeftRight.forEach((signal) -> {
250        signal.setHeld(held);
251        });
252}
253
254    private void setSignalDirectionIndicatorsToFieldSignalsState() {
255        setSignalDirectionIndicatorsToDirection(getSignalsInTheFieldDirection());
256    }
257
258    private void setSignalDirectionIndicatorsToDirection(int direction) {
259        switch (direction) {
260            case CTCConstants.RIGHTTRAFFIC:
261                _mLeftSensor.setKnownState(Sensor.INACTIVE);
262                _mNormalSensor.setKnownState(Sensor.INACTIVE);
263                _mRightSensor.setKnownState(Sensor.ACTIVE);
264                break;
265            case CTCConstants.LEFTTRAFFIC:
266                _mLeftSensor.setKnownState(Sensor.ACTIVE);
267                _mNormalSensor.setKnownState(Sensor.INACTIVE);
268                _mRightSensor.setKnownState(Sensor.INACTIVE);
269                break;
270            case CTCConstants.SIGNALSNORMAL:
271                _mLeftSensor.setKnownState(Sensor.INACTIVE);
272                _mNormalSensor.setKnownState(Sensor.ACTIVE);
273                _mRightSensor.setKnownState(Sensor.INACTIVE);
274                break;
275            default: // Either OUTOFCORRESPONDENCE or invalid passed value:
276                _mLeftSensor.setKnownState(Sensor.INACTIVE);
277                _mNormalSensor.setKnownState(Sensor.INACTIVE);
278                _mRightSensor.setKnownState(Sensor.INACTIVE);
279                break;
280        }
281        _mPresentDirection = direction;
282    }
283
284    private void timeLockingDone() {
285        setSignalDirectionIndicatorsToFieldSignalsState();  // They ALWAYS reflect the field, even if error!
286        cancelLockedRoute();
287    }
288
289//  Called by "codingTime" object when it's timer fires:
290    private void codingTimeDone() {
291        if (!isRunningTime()) { // Not running time, signals can change dynamically:
292/*
293    In "CodeButtonPressed", we have taken off the "held" bits if a direction was requested.  The ABS system
294    then takes over and attempts to change the signal.  And since some time has passed ("codingTimeInMilliseconds"),
295    the signals have had a - chance - to change indication from red.  At this moment in time, if the signal is still
296    red, we will set to held all non held signals that are still red.  In this way, if the Dispatcher coded
297    a signal for right traffic, and the block to the right was occupied, and we took off the held bit, the
298    signal would stay red, but NOT be held.  Then if the block to the right became un-occupied, the signal would
299    change to non-red.  This is NOT what Rick Moser wants in discussion on 1/19/17.  He said that the signal should
300    REMAIN red even if occupancy goes clear (non fleeting).
301*/
302//  A way to test if "cancelLockedRoute();" below is called.  Make our signal system inconsistent with
303//  our route allocation logic, to verify if the signal system stays red, we deallocate our allocation earlier.
304//          for (NBHAbstractSignalCommon signal : _mSignalListLeftRight) {
305//              signal.setAppearance(SignalHead.RED);
306//          }
307            if (allSignalsRedSetThemAllHeld(_mRequestedDirectionObserver.getRequestedDirection())) {
308                cancelLockedRoute();
309            }
310            setSignalDirectionIndicatorsToFieldSignalsState();  // They ALWAYS reflect the field, even if error!
311        }
312    }
313
314    private void cancelLockedRoute() {
315        if (_mCodeButtonHandler != null) { _mCodeButtonHandler.cancelLockedRoute(); }
316    }
317
318//  We return an indication of whether or not all signals are red.
319//  If true, then they all were red, else false.  If requestedDirection is not left or right, then default "true" (fail safe)!
320    private boolean allSignalsRedSetThemAllHeld(int requestedDirection) {
321        if (requestedDirection == CTCConstants.LEFTTRAFFIC) {
322            boolean allRed = true;
323            for (NBHSignal signal : _mSignalListRightLeft) {   // Can't use lambda here!
324                if (!signal.isDanger()) { allRed = false; break; }
325            }
326            if (allRed) { _mSignalListRightLeft.forEach((signalHead) -> signalHead.setHeld(true)); }
327            return allRed;
328        } else if (requestedDirection == CTCConstants.RIGHTTRAFFIC) {
329            boolean allRed = true;
330            for (NBHSignal signal : _mSignalListLeftRight) {   // Can't use lambda here!
331                if (!signal.isDanger()) { allRed = false; break; }
332            }
333            if (allRed) { _mSignalListLeftRight.forEach((signalHead) -> signalHead.setHeld(true)); }
334            return allRed;
335        }
336        return true;
337    }
338
339/*  With the introduction of SignalMast objects, I had to modify this routine
340    to support them ("changedToUniversalRed"):
341*/
342    private void handleSignalChange(PropertyChangeEvent e) {
343        if (_mFleetingObject != null) {
344            if (!_mFleetingObject.isFleetingEnabled()) {
345                if (changedToUniversalRed(e)) {    // Signal (SignalMast, SignalHead) changed to Red:
346                    boolean forceAllSignalsToHeld = false;
347                    if (_mPresentSignalDirectionLever == CTCConstants.RIGHTTRAFFIC) {
348                        for (NBHSignal signal : _mSignalListLeftRight) {
349                            if (e.getSource() == signal.getBean()) {
350                                forceAllSignalsToHeld = true;
351                                break;
352                            }
353                        }
354                    } else if (_mPresentSignalDirectionLever == CTCConstants.LEFTTRAFFIC) {
355                        for (NBHSignal signal : _mSignalListRightLeft) {
356                            if (e.getSource() == signal.getBean()) {
357                                forceAllSignalsToHeld = true;
358                                break;
359                            }
360                        }
361                    }
362                    if (forceAllSignalsToHeld) forceAllSignalsToHeld();
363                }
364            }
365        }
366        possiblyUpdateSignalIndicationSensors();
367    }
368
369    private boolean changedToUniversalRed(PropertyChangeEvent e) {
370        Object source = e.getSource();
371        if (source instanceof AbstractSignalHead) {
372            if (e.getPropertyName().equals("Appearance")) { // NOI18N
373                return SignalHead.RED == (int)e.getNewValue();
374            }
375        } else if (source instanceof AbstractSignalMast) {
376            if (e.getPropertyName().equals("Aspect")) { // NOI18N
377                AbstractSignalMast source2 = (AbstractSignalMast)source;
378                String source2Aspect = source2.getAspect();
379                return source2Aspect != null && 
380                    source2Aspect.equals(source2.getAppearanceMap().getSpecificAppearance(SignalAppearanceMap.DANGER));
381            }
382        }
383        return false;   // If none of the above, don't know, assume not red.
384    }
385
386    private void possiblyUpdateSignalIndicationSensors() {
387        if (!_mCodingTimeTimer.isRunning() && !isRunningTime()) {   // Not waiting for coding time and not running time, signals can change dynamically:
388            setSignalDirectionIndicatorsToFieldSignalsState();      // They ALWAYS reflect the field, even if error!
389        }
390    }
391}