001package jmri.jmrit.logixng.actions;
002
003import java.beans.*;
004import java.util.*;
005
006import jmri.*;
007import jmri.jmrit.logixng.*;
008import jmri.util.TimerUtil;
009
010/**
011 * Simulates turnout feedback.
012 * @author Daniel Bergqvist (C) 2022
013 */
014public class SimulateTurnoutFeedback extends AbstractDigitalAction
015        implements PropertyChangeListener, VetoableChangeListener {
016
017    private final TurnoutListener _turnoutListener = new TurnoutListener();
018    private final Map<Turnout, TurnoutTimerTask> _timerTasks = new HashMap<>();
019
020    private int _delay = 3;     // Delay in seconds
021    private final Map<String, TurnoutInfo> _turnouts = new HashMap<>();
022    private boolean _hasBeenExecuted = false;
023
024
025    public SimulateTurnoutFeedback(String sys, String user)
026            throws BadUserNameException, BadSystemNameException {
027        super(sys, user, Category.OTHER);
028    }
029
030    @Override
031    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
032        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
033        String sysName = systemNames.get(getSystemName());
034        String userName = userNames.get(getSystemName());
035        if (sysName == null) sysName = manager.getAutoSystemName();
036        SimulateTurnoutFeedback copy = new SimulateTurnoutFeedback(sysName, userName);
037        copy.setComment(getComment());
038        copy._delay = _delay;
039        return manager.registerAction(copy);
040    }
041
042    @Override
043    public String getShortDescription(Locale locale) {
044        return Bundle.getMessage(locale, "SimulateTurnoutFeedback_Short");
045    }
046
047    @Override
048    public String getLongDescription(Locale locale) {
049        return Bundle.getMessage(locale, "SimulateTurnoutFeedback_Long", _delay);
050    }
051
052    @Override
053    public void setup() {
054        // Do nothing
055    }
056
057    @Override
058    public void execute() throws JmriException {
059        if (!_hasBeenExecuted && _listenersAreRegistered) {
060            registerTurnoutListeners();
061        }
062        _hasBeenExecuted = true;
063    }
064
065    private void registerTurnoutListeners() {
066        TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class);
067        for (Turnout t : tm.getNamedBeanSet()) {
068            addTurnoutListener(t);
069        }
070        tm.addPropertyChangeListener("beans", this);
071        tm.addVetoableChangeListener(this);
072    }
073
074    /** {@inheritDoc} */
075    @Override
076    public synchronized void registerListenersForThisClass() {
077        if (!_listenersAreRegistered) {
078            _listenersAreRegistered = true;
079            if (_hasBeenExecuted) {
080                registerTurnoutListeners();
081            }
082        }
083    }
084
085    /** {@inheritDoc} */
086    @Override
087    public synchronized void unregisterListenersForThisClass() {
088        if (_listenersAreRegistered) {
089            TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class);
090            for (Turnout t : tm.getNamedBeanSet()) {
091                removeTurnoutListener(t);
092            }
093            tm.removePropertyChangeListener("beans", this);
094            tm.removeVetoableChangeListener(this);
095            _listenersAreRegistered = false;
096        }
097    }
098
099    private boolean hasTurnoutFeedback(Turnout t) {
100        switch (t.getFeedbackMode()) {
101            case Turnout.DIRECT:
102            case Turnout.SIGNAL:
103            case Turnout.DELAYED:
104                return false;
105
106            case Turnout.ONESENSOR:
107                return t.getFirstSensor() != null;
108
109            case Turnout.TWOSENSOR:
110                return t.getFirstSensor() != null && t.getSecondSensor() != null;
111
112            case Turnout.EXACT:
113            case Turnout.INDIRECT:
114            case Turnout.MONITORING:
115            case Turnout.LNALTERNATE:
116                return true;
117
118            default:
119                log.debug("Unsupported turnout feedback mode: {}, {}", t.getFeedbackMode(), t.getFeedbackModeName());
120                return false;
121        }
122    }
123
124    private void addTurnoutListener(Turnout turnout) {
125        if (!_turnouts.containsKey(turnout.getSystemName())) {
126            TurnoutInfo ti = new TurnoutInfo(turnout);
127            _turnouts.put(turnout.getSystemName(), ti);
128            turnout.addPropertyChangeListener("feedbackchange", this);
129            turnout.addPropertyChangeListener("turnoutFeedbackFirstSensorChange", this);
130            turnout.addPropertyChangeListener("turnoutFeedbackSecondSensorChange", this);
131            if (hasTurnoutFeedback(turnout)) {
132                turnout.addPropertyChangeListener("CommandedState", _turnoutListener);
133                ti._hasListener = true;
134            }
135        }
136    }
137
138    private void removeTurnoutListener(Turnout turnout) {
139        TurnoutInfo ti = _turnouts.remove(turnout.getSystemName());
140        turnout.removePropertyChangeListener("feedbackchange", this);
141        turnout.removePropertyChangeListener("turnoutFeedbackFirstSensorChange", this);
142        turnout.removePropertyChangeListener("turnoutFeedbackSecondSensorChange", this);
143        if (ti != null && ti._hasListener) {
144            turnout.removePropertyChangeListener("CommandedState", _turnoutListener);
145            ti._hasListener = false;
146        }
147    }
148
149    /** {@inheritDoc} */
150    @Override
151    public synchronized void propertyChange(PropertyChangeEvent evt) {
152        if (evt.getPropertyName().equals("beans")) {
153            if (!(evt.getSource() instanceof TurnoutManager)) return;
154            TurnoutManager manager = (TurnoutManager)evt.getSource();
155            if (evt.getNewValue() != null) {
156                String sysName = evt.getNewValue().toString();
157                Turnout turnout = manager.getBySystemName(sysName);
158                if (_listenersAreRegistered && (turnout != null)) {
159                    addTurnoutListener(turnout);
160                }
161            } else if (evt.getOldValue() != null) {
162                String sysName = evt.getOldValue().toString();
163                TurnoutInfo turnoutInfo = _turnouts.get(sysName);
164                if (_listenersAreRegistered
165                        && (turnoutInfo != null)
166                        && (turnoutInfo._turnout != null)) {
167                    removeTurnoutListener(turnoutInfo._turnout);
168                }
169            }
170        }
171
172        if (evt.getPropertyName().equals("feedbackchange")
173                || evt.getPropertyName().equals("turnoutFeedbackFirstSensorChange")
174                || evt.getPropertyName().equals("turnoutFeedbackSecondSensorChange")) {
175
176            TurnoutInfo ti = _turnouts.get(evt.getSource().toString());
177            if (hasTurnoutFeedback(ti._turnout)) {
178                if (!ti._hasListener) {
179                    ti._turnout.addPropertyChangeListener("CommandedState", _turnoutListener);
180                    ti._hasListener = true;
181                }
182            } else {
183                if (ti._hasListener) {
184                    ti._turnout.removePropertyChangeListener("CommandedState", _turnoutListener);
185                    ti._hasListener = false;
186                }
187            }
188        }
189    }
190
191    @Override
192    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
193        if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
194            if (evt.getOldValue() instanceof Turnout) {
195                removeTurnoutListener((Turnout) evt.getOldValue());
196            }
197        }
198    }
199
200    /** {@inheritDoc} */
201    @Override
202    public void disposeMe() {
203    }
204
205
206    private static class TurnoutInfo {
207
208        private final Turnout _turnout;
209        private boolean _hasListener;
210
211        TurnoutInfo(Turnout turnout) {
212            _turnout = turnout;
213        }
214    }
215
216
217    private class TurnoutListener implements PropertyChangeListener {
218
219        private void manageTurnout(Turnout t, int newState) {
220            synchronized (_timerTasks) {
221                if (_timerTasks.containsKey(t)) {
222                    TurnoutTimerTask task = _timerTasks.get(t);
223                    task.cancel();
224                    _timerTasks.remove(t);
225                }
226                TurnoutTimerTask task = new TurnoutTimerTask(t, newState);
227                _timerTasks.put(t, task);
228                TimerUtil.schedule(task, _delay * 1000L);
229            }
230        }
231
232        @Override
233        public void propertyChange(PropertyChangeEvent evt) {
234//            System.out.format("Source: %s, name: %s, old: %s, new: %s%n", evt.getSource(), evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
235
236            if (evt.getPropertyName().equals("CommandedState")) {
237                String sysName = evt.getSource().toString();
238                TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class);
239                Turnout t = tm.getBySystemName(sysName);
240                if (t == null) {
241                    log.error("Turnout {} does not exists in manager {}", sysName, tm);
242                    return;
243                }
244
245                switch (t.getFeedbackMode()) {
246                    case Turnout.DIRECT:
247                    case Turnout.SIGNAL:
248                    case Turnout.DELAYED:
249                        // Do nothing
250                        break;
251
252                    case Turnout.EXACT:
253                    case Turnout.INDIRECT:
254                    case Turnout.MONITORING:
255                    case Turnout.ONESENSOR:
256                    case Turnout.TWOSENSOR:
257                    case Turnout.LNALTERNATE:
258                        // Hardware feedback
259                        manageTurnout(t, (int) evt.getNewValue());
260                        break;
261
262                    default:
263                        log.debug("Unsupported turnout feedback mode: {}, {}", t.getFeedbackMode(), t.getFeedbackModeName());
264                }
265            }
266        }
267
268    }
269
270    private class TurnoutTimerTask extends java.util.TimerTask {
271
272        private final Turnout _turnout;
273        private final int _newState;
274
275        private TurnoutTimerTask(Turnout t, int newState) {
276            _turnout = t;
277            _newState = newState;
278            startMove();
279        }
280
281        private void startMove() {
282            Sensor sensor1;
283            Sensor sensor2;
284
285            switch (_turnout.getFeedbackMode()) {
286                case Turnout.EXACT:
287                    // Hardware feedback
288                    break;
289
290                case Turnout.INDIRECT:
291                    // Hardware feedback
292                    break;
293
294                case Turnout.MONITORING:
295                    // Hardware feedback
296                    break;
297
298                case Turnout.ONESENSOR:
299                    // Do nothing
300                    break;
301
302                case Turnout.TWOSENSOR:
303                    sensor1 = _turnout.getFirstSensor();
304                    if (sensor1 != null) sensor1.setCommandedState(Sensor.INACTIVE);
305                    sensor2 = _turnout.getSecondSensor();
306                    if (sensor2 != null) sensor2.setCommandedState(Sensor.INACTIVE);
307                    break;
308
309                case Turnout.LNALTERNATE:
310                    // Hardware feedback
311                    break;
312
313                default:
314                    log.debug("Unsupported turnout feedback mode: {}, {}", _turnout.getFeedbackMode(), _turnout.getFeedbackModeName());
315            }
316        }
317
318        private void stopMove() {
319            Sensor sensor;
320
321            switch (_turnout.getFeedbackMode()) {
322                case Turnout.EXACT:
323                    if (_turnout instanceof jmri.jmrix.loconet.LnTurnout) {
324                        jmri.jmrix.loconet.LnTurnout lnTurnout = (jmri.jmrix.loconet.LnTurnout)_turnout;
325                        lnTurnout.newKnownState(_newState);
326                    } else if (_turnout instanceof jmri.jmrix.lenz.XNetTurnout) {
327                        jmri.jmrix.lenz.XNetTurnout xnetTurnout = (jmri.jmrix.lenz.XNetTurnout)_turnout;
328                        xnetTurnout.newKnownState(_newState);
329                    } else {
330                        log.warn("Unknown type of turnout {}, {}", _turnout.getSystemName(), _turnout.getDisplayName());
331                    }
332                    break;
333
334                case Turnout.INDIRECT:
335                    // Hardware feedback
336                    break;
337
338                case Turnout.MONITORING:
339                    // Hardware feedback
340                    break;
341
342                case Turnout.ONESENSOR:
343                    sensor = _turnout.getFirstSensor();
344                    if (_newState == Turnout.CLOSED) {
345                        if (sensor != null) sensor.setCommandedState(Sensor.INACTIVE);
346                    } else if (_newState == Turnout.THROWN) {
347                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
348                    }
349                    break;
350
351                case Turnout.TWOSENSOR:
352                    if (_newState == Turnout.CLOSED) {
353                        sensor = _turnout.getSecondSensor();
354                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
355                    } else if (_newState == Turnout.THROWN) {
356                        sensor = _turnout.getFirstSensor();
357                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
358                    }
359                    break;
360
361                case Turnout.LNALTERNATE:
362                    // Hardware feedback
363                    break;
364
365                default:
366                    log.debug("Unsupported turnout feedback mode: {}, {}", _turnout.getFeedbackMode(), _turnout.getFeedbackModeName());
367            }
368        }
369
370        @Override
371        public void run() {
372            synchronized (_timerTasks) {
373                _timerTasks.remove(_turnout);
374            }
375            stopMove();
376        }
377    }
378
379
380    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SimulateTurnoutFeedback.class);
381}