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}