001package jmri.implementation;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.event.ActionEvent;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import jmri.NamedBeanHandle;
008import jmri.Turnout;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Drive a single signal head via two "Turnout" objects.
014 * <p>
015 * After much confusion, the user-level terminology was changed to call these
016 * "Double Output"; the class name remains the same to reduce recoding.
017 * <p>
018 * The two Turnout objects are provided during construction, and each drives a
019 * specific color (RED and GREEN). Normally, "THROWN" is on, and "CLOSED" is
020 * off. YELLOW is provided by turning both on ("THROWN")
021 * <p>
022 * This class also listens to the Turnouts to see if they've been
023 * changed via some other mechanism.
024 *
025 * @author Bob Jacobsen Copyright (C) 2003, 2008
026 */
027public class DoubleTurnoutSignalHead extends DefaultSignalHead {
028
029    public DoubleTurnoutSignalHead(String sys, String user, NamedBeanHandle<Turnout> green, NamedBeanHandle<Turnout> red) {
030        super(sys, user);
031        setRed(red);
032        setGreen(green);
033    }
034
035    public DoubleTurnoutSignalHead(String sys, NamedBeanHandle<Turnout> green, NamedBeanHandle<Turnout> red) {
036        super(sys);
037        setRed(red);
038        setGreen(green);
039    }
040
041    @SuppressWarnings("fallthrough")
042    @SuppressFBWarnings(value = "SF_SWITCH_FALLTHROUGH")
043    @Override
044    protected void updateOutput() {
045        // assumes that writing a turnout to an existing state is cheap!
046        if (mLit == false) {
047            commandState(Turnout.CLOSED, Turnout.CLOSED);
048            return;
049        } else if (!mFlashOn
050                && ((mAppearance == FLASHGREEN)
051                || (mAppearance == FLASHYELLOW)
052                || (mAppearance == FLASHRED))) {
053            // flash says to make output dark
054            commandState(Turnout.CLOSED, Turnout.CLOSED);
055            return;
056
057        } else {
058            switch (mAppearance) {
059                case RED:
060                case FLASHRED:
061                    commandState(Turnout.THROWN, Turnout.CLOSED);
062                    break;
063                case YELLOW:
064                case FLASHYELLOW:
065                    commandState(Turnout.THROWN, Turnout.THROWN);
066                    break;
067                case GREEN:
068                case FLASHGREEN:
069                    commandState(Turnout.CLOSED, Turnout.THROWN);
070                    break;
071                default:
072                    log.warn("Unexpected new appearance: {}", mAppearance);
073                // go dark by falling through
074                case DARK:
075                    commandState(Turnout.CLOSED, Turnout.CLOSED);
076                    break;
077            }
078        }
079    }
080
081    /**
082     * Sets the output turnouts' commanded state.
083     *
084     * @param red   state to set the mRed turnout
085     * @param green state to set the mGreen turnout.
086     */
087    void commandState(int red, int green) {
088        mRedCommanded = red;
089        mRed.getBean().setCommandedState(red);
090        mGreenCommanded = green;
091        mGreen.getBean().setCommandedState(green);
092    }
093
094    /**
095     * Remove references to and from this object, so that it can eventually be
096     * garbage-collected.
097     */
098    @Override
099    public void dispose() {
100        if (mRed != null) {
101            mRed.getBean().removePropertyChangeListener(turnoutChangeListener);
102        }
103        if (mGreen != null) {
104            mGreen.getBean().removePropertyChangeListener(turnoutChangeListener);
105        }
106        mRed = null;
107        mGreen = null;
108        jmri.InstanceManager.turnoutManagerInstance().removeVetoableChangeListener(this);
109        super.dispose();
110    }
111
112    NamedBeanHandle<Turnout> mRed;
113    NamedBeanHandle<Turnout> mGreen;
114    int mRedCommanded;
115    int mGreenCommanded;
116
117    public NamedBeanHandle<Turnout> getRed() {
118        return mRed;
119    }
120
121    public NamedBeanHandle<Turnout> getGreen() {
122        return mGreen;
123    }
124
125    public void setRed(NamedBeanHandle<Turnout> t) {
126        if (mRed != null) {
127            mRed.getBean().removePropertyChangeListener(turnoutChangeListener);
128        }
129        mRed = t;
130        if (mRed != null) {
131            mRed.getBean().addPropertyChangeListener(turnoutChangeListener);
132        }
133    }
134
135    public void setGreen(NamedBeanHandle<Turnout> t) {
136        if (mGreen != null) {
137            mGreen.getBean().removePropertyChangeListener(turnoutChangeListener);
138        }
139        mGreen = t;
140        if (mGreen != null) {
141            mGreen.getBean().addPropertyChangeListener(turnoutChangeListener);
142        }
143    }
144
145    @Override
146    boolean isTurnoutUsed(Turnout t) {
147        if (getRed() != null && t.equals(getRed().getBean())) {
148            return true;
149        }
150        if (getGreen() != null && t.equals(getGreen().getBean())) {
151            return true;
152        }
153        return false;
154    }
155
156    javax.swing.Timer readUpdateTimer;
157
158    private PropertyChangeListener turnoutChangeListener = new PropertyChangeListener() {
159        @Override
160        public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
161            if (propertyChangeEvent.getPropertyName().equals("KnownState")) {
162                if (propertyChangeEvent.getSource().equals(mRed.getBean()) && propertyChangeEvent.getNewValue().equals(mRedCommanded)) {
163                    return; // ignore change that we commanded
164                }
165                if (propertyChangeEvent.getSource().equals(mGreen.getBean()) && propertyChangeEvent.getNewValue().equals(mGreenCommanded)) {
166                    return; // ignore change that we commanded
167                }
168                if (readUpdateTimer == null) {
169                    readUpdateTimer = new javax.swing.Timer(200, (ActionEvent actionEvent) ->
170                            readOutput());
171                    readUpdateTimer.setRepeats(false);
172                    readUpdateTimer.start();
173                } else {
174                    readUpdateTimer.restart();
175                }
176            }
177        }
178    };
179
180    /**
181     * Checks if the turnouts' output state matches the commanded output state; if not, then
182     * changes the appearance to match the output's current state.
183     */
184    void readOutput() {
185        if ((mAppearance == FLASHGREEN)
186                || (mAppearance == FLASHYELLOW)
187                || (mAppearance == FLASHRED)
188                || (mAppearance == FLASHLUNAR)) {
189            // If we are actively flashing right now, then we ignore external changes, since
190            // those might be coming from ourselves and will be overwritten shortly.
191            return;
192        }
193        int red = mRed.getBean().getKnownState();
194        int green = mGreen.getBean().getKnownState();
195        if (mRedCommanded == red && mGreenCommanded == green) return;
196        // The turnouts' known state has diverged from what we set. We attempt to decode the
197        // actual state to an appearance. This is a lossy operation, but the user has done
198        // something very explicitly to make this happen, like manually clicking the turnout throw
199        // button, or setting up an external signaling logic system.
200        if (red == Turnout.CLOSED && green == Turnout.CLOSED) {
201            setAppearance(DARK);
202        } else if (red == Turnout.THROWN && green == Turnout.CLOSED) {
203            setAppearance(RED);
204        } else if (red == Turnout.THROWN && green == Turnout.THROWN) {
205            setAppearance(YELLOW);
206        } else if (red == Turnout.CLOSED && green == Turnout.THROWN) {
207            setAppearance(GREEN);
208        }
209    }
210
211    private final static Logger log = LoggerFactory.getLogger(DoubleTurnoutSignalHead.class);
212}