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