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(Manager.PROPERTY_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(Manager.PROPERTY_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(Turnout.PROPERTY_FEEDBACK_MODE, this);
129            turnout.addPropertyChangeListener(Turnout.PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR, this);
130            turnout.addPropertyChangeListener(Turnout.PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR, this);
131            if (hasTurnoutFeedback(turnout)) {
132                turnout.addPropertyChangeListener(Turnout.PROPERTY_COMMANDED_STATE, _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(Turnout.PROPERTY_FEEDBACK_MODE, this);
141        turnout.removePropertyChangeListener(Turnout.PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR, this);
142        turnout.removePropertyChangeListener(Turnout.PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR, this);
143        if (ti != null && ti._hasListener) {
144            turnout.removePropertyChangeListener(Turnout.PROPERTY_COMMANDED_STATE, _turnoutListener);
145            ti._hasListener = false;
146        }
147    }
148
149    /** {@inheritDoc} */
150    @Override
151    public synchronized void propertyChange(PropertyChangeEvent evt) {
152        if (Manager.PROPERTY_BEANS.equals(evt.getPropertyName())) {
153            if (!(evt.getSource() instanceof TurnoutManager)) {
154                log.error("Non-TurnoutManager sent event : {}", evt);
155                return;
156            }
157            TurnoutManager manager = (TurnoutManager)evt.getSource();
158            if (evt.getNewValue() != null) {
159                String sysName = evt.getNewValue().toString();
160                Turnout turnout = manager.getBySystemName(sysName);
161                if (_listenersAreRegistered && (turnout != null)) {
162                    addTurnoutListener(turnout);
163                }
164            } else if (evt.getOldValue() != null) {
165                String sysName = evt.getOldValue().toString();
166                TurnoutInfo turnoutInfo = _turnouts.get(sysName);
167                if (_listenersAreRegistered
168                        && (turnoutInfo != null)
169                        && (turnoutInfo._turnout != null)) {
170                    removeTurnoutListener(turnoutInfo._turnout);
171                }
172            }
173        }
174
175        String evp = evt.getPropertyName();
176        if ( Turnout.PROPERTY_FEEDBACK_MODE.equals(evp)
177                || Turnout.PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR.equals(evp)
178                || Turnout.PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR.equals(evp)) {
179
180            TurnoutInfo ti = _turnouts.get(evt.getSource().toString());
181            if (hasTurnoutFeedback(ti._turnout)) {
182                if (!ti._hasListener) {
183                    ti._turnout.addPropertyChangeListener(Turnout.PROPERTY_COMMANDED_STATE, _turnoutListener);
184                    ti._hasListener = true;
185                }
186            } else {
187                if (ti._hasListener) {
188                    ti._turnout.removePropertyChangeListener(Turnout.PROPERTY_COMMANDED_STATE, _turnoutListener);
189                    ti._hasListener = false;
190                }
191            }
192        }
193    }
194
195    @Override
196    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
197        if ( Manager.PROPERTY_DO_DELETE.equals(evt.getPropertyName())
198            && evt.getOldValue() instanceof Turnout ) {
199                removeTurnoutListener((Turnout) evt.getOldValue());
200        }
201    }
202
203    /** {@inheritDoc} */
204    @Override
205    public void disposeMe() {
206    }
207
208
209    private static class TurnoutInfo {
210
211        private final Turnout _turnout;
212        private boolean _hasListener;
213
214        TurnoutInfo(Turnout turnout) {
215            _turnout = turnout;
216        }
217    }
218
219
220    private class TurnoutListener implements PropertyChangeListener {
221
222        private void manageTurnout(Turnout t, int newState) {
223            synchronized (_timerTasks) {
224                if (_timerTasks.containsKey(t)) {
225                    TurnoutTimerTask task = _timerTasks.get(t);
226                    task.cancel();
227                    _timerTasks.remove(t);
228                }
229                TurnoutTimerTask task = new TurnoutTimerTask(t, newState);
230                _timerTasks.put(t, task);
231                TimerUtil.schedule(task, _delay * 1000L);
232            }
233        }
234
235        @Override
236        public void propertyChange(PropertyChangeEvent evt) {
237//            System.out.format("Source: %s, name: %s, old: %s, new: %s%n", evt.getSource(), evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
238
239            if ( Turnout.PROPERTY_COMMANDED_STATE.equals(evt.getPropertyName())) {
240                String sysName = evt.getSource().toString();
241                TurnoutManager tm = InstanceManager.getDefault(TurnoutManager.class);
242                Turnout t = tm.getBySystemName(sysName);
243                if (t == null) {
244                    log.error("Turnout {} does not exists in manager {}", sysName, tm);
245                    return;
246                }
247
248                switch (t.getFeedbackMode()) {
249                    case Turnout.DIRECT:
250                    case Turnout.SIGNAL:
251                    case Turnout.DELAYED:
252                        // Do nothing
253                        break;
254
255                    case Turnout.EXACT:
256                    case Turnout.INDIRECT:
257                    case Turnout.MONITORING:
258                    case Turnout.ONESENSOR:
259                    case Turnout.TWOSENSOR:
260                    case Turnout.LNALTERNATE:
261                        // Hardware feedback
262                        manageTurnout(t, (int) evt.getNewValue());
263                        break;
264
265                    default:
266                        log.debug("Unsupported turnout feedback mode: {}, {}", t.getFeedbackMode(), t.getFeedbackModeName());
267                }
268            }
269        }
270
271    }
272
273    private class TurnoutTimerTask extends java.util.TimerTask {
274
275        private final Turnout _turnout;
276        private final int _newState;
277
278        private TurnoutTimerTask(Turnout t, int newState) {
279            _turnout = t;
280            _newState = newState;
281            startMove();
282        }
283
284        private void startMove() {
285            Sensor sensor1;
286            Sensor sensor2;
287
288            switch (_turnout.getFeedbackMode()) {
289                case Turnout.EXACT:
290                    // Hardware feedback
291                    break;
292
293                case Turnout.INDIRECT:
294                    // Hardware feedback
295                    break;
296
297                case Turnout.MONITORING:
298                    // Hardware feedback
299                    break;
300
301                case Turnout.ONESENSOR:
302                    // Do nothing
303                    break;
304
305                case Turnout.TWOSENSOR:
306                    sensor1 = _turnout.getFirstSensor();
307                    if (sensor1 != null) sensor1.setCommandedState(Sensor.INACTIVE);
308                    sensor2 = _turnout.getSecondSensor();
309                    if (sensor2 != null) sensor2.setCommandedState(Sensor.INACTIVE);
310                    break;
311
312                case Turnout.LNALTERNATE:
313                    // Hardware feedback
314                    break;
315
316                default:
317                    log.debug("Unsupported turnout feedback mode: {}, {}", _turnout.getFeedbackMode(), _turnout.getFeedbackModeName());
318            }
319        }
320
321        private void stopMove() {
322            Sensor sensor;
323
324            switch (_turnout.getFeedbackMode()) {
325                case Turnout.EXACT:
326                    if (_turnout instanceof jmri.jmrix.loconet.LnTurnout) {
327                        jmri.jmrix.loconet.LnTurnout lnTurnout = (jmri.jmrix.loconet.LnTurnout)_turnout;
328                        lnTurnout.newKnownState(_newState);
329                    } else if (_turnout instanceof jmri.jmrix.lenz.XNetTurnout) {
330                        jmri.jmrix.lenz.XNetTurnout xnetTurnout = (jmri.jmrix.lenz.XNetTurnout)_turnout;
331                        xnetTurnout.newKnownState(_newState);
332                    } else {
333                        log.warn("Unknown type of turnout {}, {}", _turnout.getSystemName(), _turnout.getDisplayName());
334                    }
335                    break;
336
337                case Turnout.INDIRECT:
338                    // Hardware feedback
339                    break;
340
341                case Turnout.MONITORING:
342                    // Hardware feedback
343                    break;
344
345                case Turnout.ONESENSOR:
346                    sensor = _turnout.getFirstSensor();
347                    if (_newState == Turnout.CLOSED) {
348                        if (sensor != null) sensor.setCommandedState(Sensor.INACTIVE);
349                    } else if (_newState == Turnout.THROWN) {
350                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
351                    }
352                    break;
353
354                case Turnout.TWOSENSOR:
355                    if (_newState == Turnout.CLOSED) {
356                        sensor = _turnout.getSecondSensor();
357                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
358                    } else if (_newState == Turnout.THROWN) {
359                        sensor = _turnout.getFirstSensor();
360                        if (sensor != null) sensor.setCommandedState(Sensor.ACTIVE);
361                    }
362                    break;
363
364                case Turnout.LNALTERNATE:
365                    // Hardware feedback
366                    break;
367
368                default:
369                    log.debug("Unsupported turnout feedback mode: {}, {}", _turnout.getFeedbackMode(), _turnout.getFeedbackModeName());
370            }
371        }
372
373        @Override
374        public void run() {
375            synchronized (_timerTasks) {
376                _timerTasks.remove(_turnout);
377            }
378            stopMove();
379        }
380    }
381
382
383    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SimulateTurnoutFeedback.class);
384}