001package jmri.implementation; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.beans.PropertyChangeEvent; 005import java.beans.PropertyChangeListener; 006import jmri.NamedBeanHandle; 007import jmri.SignalHead; 008import jmri.Turnout; 009 010/** 011 * Drive a single signal head via one "Turnout" object. 012 * <p> 013 * After much confusion, the user-level terminology was changed to call these 014 * "Single Output"; the class name remains the same to reduce recoding. 015 * <p> 016 * One Turnout object is provided during construction, and drives the appearance 017 * to be either ON or OFF. Normally, "THROWN" is on, and "CLOSED" is off. The 018 * facility to set the appearance via any of the basic four appearance colors + 019 * Lunar is provided, however they all do the same. 020 * <p> 021 * Based upon DoubleTurnoutSignalHead by Bob Jacobsen 022 * 023 * @author Kevin Dickerson Copyright (C) 2010 024 */ 025public class SingleTurnoutSignalHead extends DefaultSignalHead implements PropertyChangeListener { 026 027 /** 028 * Ctor including user name. 029 * 030 * @param sys system name for head 031 * @param user userName user name for head 032 * @param lit named bean for turnout switching the Lit property 033 * @param on Appearance constant from {@link jmri.SignalHead} for the 034 * output on (Turnout thrown) appearance 035 * @param off Appearance constant from {@link jmri.SignalHead} for the 036 * signal off (Turnout closed) appearance 037 */ 038 public SingleTurnoutSignalHead(String sys, String user, NamedBeanHandle<Turnout> lit, int on, int off) { 039 super(sys, user); 040 initialize(lit, on, off); 041 } 042 043 /** 044 * Ctor using only a system name. 045 * 046 * @param sys system name for head 047 * @param lit named bean for turnout switching the Lit property 048 * @param on Appearance constant from {@link jmri.SignalHead} for the 049 * output on (Turnout thrown) appearance 050 * @param off Appearance constant from {@link jmri.SignalHead} for the 051 * signal off (Turnout closed) appearance 052 */ 053 public SingleTurnoutSignalHead(String sys, NamedBeanHandle<Turnout> lit, int on, int off) { 054 super(sys); 055 initialize(lit, on, off); 056 } 057 058 /** 059 * Helper function for constructors. 060 * 061 * @param lit named bean for turnout switching the Lit property 062 * @param on Appearance constant from {@link jmri.SignalHead} for the 063 * output on (Turnout thrown) appearance 064 * @param off Appearance constant from {@link jmri.SignalHead} for the 065 * signal off (Turnout closed) appearance 066 */ 067 private void initialize(NamedBeanHandle<Turnout> lit, int on, int off) { 068 setOutput(lit); 069 mOnAppearance = on; 070 mOffAppearance = off; 071 switch (lit.getBean().getKnownState()) { 072 case jmri.Turnout.CLOSED: 073 setAppearance(off); 074 break; 075 case jmri.Turnout.THROWN: 076 setAppearance(on); 077 break; 078 default: 079 // Assumes "off" state to prevents setting turnouts at startup. 080 mAppearance = off; 081 break; 082 } 083 } 084 085 private int mOnAppearance = DARK; 086 private int mOffAppearance = LUNAR; 087 088 /** 089 * Holds the last state change we commanded our underlying turnout. 090 */ 091 private int mTurnoutCommandedState = Turnout.CLOSED; 092 093 private void setTurnoutState(int s) { 094 mTurnoutCommandedState = s; 095 mOutput.getBean().setCommandedState(s); 096 } 097 098 @Override 099 protected void updateOutput() { 100 // assumes that writing a turnout to an existing state is cheap! 101 if (!mLit) { 102 setTurnoutState(Turnout.CLOSED); 103 } else if (!mFlashOn && (mAppearance == mOnAppearance * 2)) { 104 setTurnoutState(Turnout.CLOSED); 105 } else if (!mFlashOn && (mAppearance == mOffAppearance * 2)) { 106 setTurnoutState(Turnout.THROWN); 107 } else { 108 if ((mAppearance == mOffAppearance) || (mAppearance == (mOffAppearance * 2))) { 109 setTurnoutState(Turnout.CLOSED); 110 } else if ((mAppearance == mOnAppearance) || (mAppearance == (mOnAppearance * 2))) { 111 setTurnoutState(Turnout.THROWN); 112 } else { 113 log.warn("Unexpected: Single Output Red / Green SignalHeads cannot display new appearance: {}", 114 describeState(mAppearance)); 115 } 116 } 117 } 118 119 /** 120 * Remove references to and from this object, so that it can eventually be 121 * garbage-collected. 122 */ 123 @Override 124 public void dispose() { 125 setOutput(null); 126 super.dispose(); 127 } 128 129 private NamedBeanHandle<Turnout> mOutput; 130 131 public int getOnAppearance() { 132 return mOnAppearance; 133 } 134 135 public int getOffAppearance() { 136 return mOffAppearance; 137 } 138 139 public void setOnAppearance(int on) { 140 int old = mOnAppearance; 141 mOnAppearance = on; 142 firePropertyChange("ValidStatesChanged", old, on); 143 } 144 145 public void setOffAppearance(int off) { 146 int old = mOffAppearance; 147 mOffAppearance = off; 148 firePropertyChange("ValidStatesChanged", old, off); 149 } 150 151 public NamedBeanHandle<Turnout> getOutput() { 152 return mOutput; 153 } 154 155 public void setOutput(NamedBeanHandle<Turnout> t) { 156 if (mOutput != null) { 157 mOutput.getBean().removePropertyChangeListener(this); 158 } 159 mOutput = t; 160 if (mOutput != null) { 161 mOutput.getBean().addPropertyChangeListener(this); 162 } 163 } 164 165 /** 166 * {@inheritDoc} 167 */ 168 @Override 169 public int[] getValidStates() { 170 int[] validStates; 171 if (mOnAppearance == mOffAppearance) { 172 validStates = new int[2]; 173 validStates[0] = mOnAppearance; 174 validStates[1] = mOffAppearance; 175 return validStates; 176 } if (mOnAppearance == DARK || mOffAppearance == DARK) { // we can make flashing with Dark only 177 validStates = new int[3]; 178 } else { 179 validStates = new int[2]; 180 } 181 int x = 0; 182 validStates[x] = mOnAppearance; 183 x++; 184 if (mOffAppearance == DARK) { 185 validStates[x] = (mOnAppearance * 2); // makes flashing of the one color 186 x++; 187 } 188 validStates[x] = mOffAppearance; 189 x++; 190 if (mOnAppearance == DARK) { 191 validStates[x] = (mOffAppearance * 2); // makes flashing of the one color 192 } 193 return validStates; 194 } 195 196 /** 197 * {@inheritDoc} 198 */ 199 @Override 200 public String[] getValidStateKeys() { 201 String[] validStateKeys = new String[getValidStates().length]; 202 int i = 0; 203 // use the logic coded in getValidStates() 204 for (int state : getValidStates()) { 205 validStateKeys[i++] = getSignalColorKey(state); 206 } 207 return validStateKeys; 208 } 209 210 /** 211 * {@inheritDoc} 212 */ 213 @Override 214 public String[] getValidStateNames() { 215 String[] validStateNames = new String[getValidStates().length]; 216 int i = 0; 217 // use the logic coded in getValidStates() 218 for (int state : getValidStates()) { 219 validStateNames[i++] = getSignalColorName(state); 220 } 221 return validStateNames; 222 } 223 224 @SuppressWarnings("fallthrough") 225 @SuppressFBWarnings(value = "SF_SWITCH_FALLTHROUGH") 226 private String getSignalColorKey(int mAppearance) { 227 switch (mAppearance) { 228 case SignalHead.RED: 229 return "SignalHeadStateRed"; 230 case SignalHead.FLASHRED: 231 return "SignalHeadStateFlashingRed"; 232 case SignalHead.YELLOW: 233 return "SignalHeadStateYellow"; 234 case SignalHead.FLASHYELLOW: 235 return "SignalHeadStateFlashingYellow"; 236 case SignalHead.GREEN: 237 return "SignalHeadStateGreen"; 238 case SignalHead.FLASHGREEN: 239 return "SignalHeadStateFlashingGreen"; 240 case SignalHead.LUNAR: 241 return "SignalHeadStateLunar"; 242 case SignalHead.FLASHLUNAR: 243 return "SignalHeadStateFlashingLunar"; 244 default: 245 log.warn("Unexpected appearance: {}", mAppearance); 246 // go dark by falling through 247 case SignalHead.DARK: 248 return "SignalHeadStateDark"; 249 } 250 } 251 252 private String getSignalColorName(int mAppearance) { 253 return Bundle.getMessage(getSignalColorKey(mAppearance)); 254 } 255 256 @Override 257 public boolean isTurnoutUsed(Turnout t) { 258 return getOutput() != null && t.equals(getOutput().getBean()); 259 } 260 261 /* (non-Javadoc) 262 * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent) 263 */ 264 @Override 265 public void propertyChange(PropertyChangeEvent evt) { 266 if (evt.getSource().equals(mOutput.getBean()) && evt.getPropertyName().equals("KnownState")) { 267 // The underlying turnout has some state change. Check if its known state matches what we expected it to do. 268 int newTurnoutValue = ((Integer) evt.getNewValue()); 269 /*String oldTurnoutString = turnoutToString(mTurnoutCommandedState); 270 String newTurnoutString = turnoutToString(newTurnoutValue); 271 log.warn("signal {}: underlying turnout changed. last set state {}, current turnout state {}, current appearance {}", 272 this.mUserName, oldTurnoutString, newTurnoutString, getSignalColour(mAppearance));*/ 273 if (mTurnoutCommandedState != newTurnoutValue) { 274 // The turnout state has changed against what we commanded. 275 int oldAppearance = mAppearance; 276 int newAppearance = mAppearance; 277 if (newTurnoutValue == Turnout.CLOSED) { 278 newAppearance = mOffAppearance; 279 } 280 if (newTurnoutValue == Turnout.THROWN) { 281 newAppearance = mOnAppearance; 282 } 283 if (newAppearance != oldAppearance) { 284 mAppearance = newAppearance; 285 // Updates last commanded state. 286 mTurnoutCommandedState = newTurnoutValue; 287 // notify listeners, if any 288 firePropertyChange("Appearance", oldAppearance, newAppearance); 289 } 290 } 291 } 292 } 293 294 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SingleTurnoutSignalHead.class); 295 296}