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 public 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}