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}