001package jmri.managers;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005
006import java.time.LocalDateTime;
007import java.time.temporal.ChronoUnit;
008import java.util.Objects;
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011import javax.annotation.OverridingMethodsMustInvokeSuper;
012
013import jmri.*;
014import jmri.implementation.SignalSpeedMap;
015import jmri.SystemConnectionMemo;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Abstract partial implementation of a TurnoutManager.
021 *
022 * @author Bob Jacobsen Copyright (C) 2001
023 */
024public abstract class AbstractTurnoutManager extends AbstractManager<Turnout>
025        implements TurnoutManager {
026
027    public AbstractTurnoutManager(SystemConnectionMemo memo) {
028        super(memo);
029        InstanceManager.getDefault(TurnoutOperationManager.class); // force creation of an instance
030        init();
031    }
032
033    final void init() {
034        InstanceManager.getDefault(SensorManager.class).addVetoableChangeListener(this);
035        // set listener for changes in memo
036        memo.addPropertyChangeListener(pcl);
037    }
038
039    final PropertyChangeListener pcl = (PropertyChangeEvent e) -> {
040        if (e.getPropertyName().equals(SystemConnectionMemo.INTERVAL)) {
041            handleIntervalChange((Integer) e.getNewValue());
042        }
043    };
044
045    /** {@inheritDoc} */
046    @Override
047    public int getXMLOrder() {
048        return Manager.TURNOUTS;
049    }
050
051    /** {@inheritDoc} */
052    @Override
053    public char typeLetter() {
054        return 'T';
055    }
056
057    /** {@inheritDoc} */
058    @Override
059    @Nonnull
060    public Turnout provideTurnout(@Nonnull String name) {
061        log.debug("provide turnout {}", name);
062        Turnout result = getTurnout(name);
063        return result == null ? newTurnout(makeSystemName(name, true), null) : result;
064    }
065
066    /** {@inheritDoc} */
067    @Override
068    @CheckForNull
069    public Turnout getTurnout(@Nonnull String name) {
070        Turnout result = getByUserName(name);
071        if (result == null) {
072            result = getBySystemName(name);
073        }
074        return result;
075    }
076
077    /** {@inheritDoc} */
078    @Override
079    @Nonnull
080    public Turnout newTurnout(@Nonnull String systemName, @CheckForNull String userName) throws IllegalArgumentException {
081        Objects.requireNonNull(systemName, "SystemName cannot be null. UserName was " + ((userName == null) ? "null" : userName));  // NOI18N
082        // add normalize? see AbstractSensor
083        log.debug("newTurnout: {};{}", systemName, userName);
084
085        // is system name in correct format?
086        if (!systemName.startsWith(getSystemNamePrefix())
087                || !(systemName.length() > (getSystemNamePrefix()).length())) {
088            log.error("Invalid system name for Turnout: {} needed {}{} followed by a suffix",
089                    systemName, getSystemPrefix(), typeLetter());
090            throw new IllegalArgumentException("Invalid system name for Turnout: " + systemName
091                    + " needed " + getSystemNamePrefix());
092        }
093
094        // return existing if there is one
095        Turnout t;
096        if (userName != null) {
097            t = getByUserName(userName);
098            if (t != null) {
099                if (getBySystemName(systemName) != t) {
100                    log.error("inconsistent user ({}) and system name ({}) results; userName related to ({})",
101                        userName, systemName, t.getSystemName());
102                }
103            return t;
104            }
105        }
106        t = getBySystemName(systemName);
107        if (t != null) {
108            if ((t.getUserName() == null) && (userName != null)) {
109                t.setUserName(userName);
110            } else if (userName != null) {
111                log.warn("Found turnout via system name ({}) with non-null user name ({}). Turnout \"{} ({})\" cannot be used.",
112                        systemName, t.getUserName(), systemName, userName);
113            }
114            return t;
115        }
116
117        // doesn't exist, make a new one
118        t = createNewTurnout(systemName, userName);
119        // if that failed, will throw an IllegalArgumentException
120
121        // Some implementations of createNewTurnout() register the new bean,
122        // some don't.
123        if (getBySystemName(t.getSystemName()) == null) {
124            // save in the maps if successful
125            register(t);
126        }
127
128        try {
129            t.setStraightSpeed("Global");
130        } catch (jmri.JmriException ex) {
131            log.error("Turnout : {} : {}", t, ex.getMessage());
132        }
133
134        try {
135            t.setDivergingSpeed("Global");
136        } catch (jmri.JmriException ex) {
137            log.error("Turnout : {} : {}", t, ex.getMessage());
138        }
139        return t;
140    }
141
142    /** {@inheritDoc} */
143    @Override
144    @Nonnull
145    public String getBeanTypeHandled(boolean plural) {
146        return Bundle.getMessage(plural ? "BeanNameTurnouts" : "BeanNameTurnout");
147    }
148
149    /**
150     * {@inheritDoc}
151     */
152    @Override
153    public Class<Turnout> getNamedBeanClass() {
154        return Turnout.class;
155    }
156
157    /** {@inheritDoc} */
158    @Override
159    @Nonnull
160    public String getClosedText() {
161        return Bundle.getMessage("TurnoutStateClosed");
162    }
163
164    /** {@inheritDoc} */
165    @Override
166    @Nonnull
167    public String getThrownText() {
168        return Bundle.getMessage("TurnoutStateThrown");
169    }
170
171    /**
172     * Get from the user, the number of addressed bits used to control a
173     * turnout. Normally this is 1, and the default routine returns 1
174     * automatically. Turnout Managers for systems that can handle multiple
175     * control bits should override this method with one which asks the user to
176     * specify the number of control bits. If the user specifies more than one
177     * control bit, this method should check if the additional bits are
178     * available (not assigned to another object). If the bits are not
179     * available, this method should return 0 for number of control bits, after
180     * informing the user of the problem.
181     */
182    @Override
183    public int askNumControlBits(@Nonnull String systemName) {
184        return 1;
185    }
186
187    /** {@inheritDoc} */
188    @Override
189    public boolean isNumControlBitsSupported(@Nonnull String systemName) {
190        return false;
191    }
192
193    /**
194     * Get from the user, the type of output to be used bits to control a
195     * turnout. Normally this is 0 for 'steady state' control, and the default
196     * routine returns 0 automatically. Turnout Managers for systems that can
197     * handle pulsed control as well as steady state control should override
198     * this method with one which asks the user to specify the type of control
199     * to be used. The routine should return 0 for 'steady state' control, or n
200     * for 'pulsed' control, where n specifies the duration of the pulse
201     * (normally in seconds).
202     */
203    @Override
204    public int askControlType(@Nonnull String systemName) {
205        return 0;
206    }
207
208    /** {@inheritDoc} */
209    @Override
210    public boolean isControlTypeSupported(@Nonnull String systemName) {
211        return false;
212    }
213
214    /**
215     * Internal method to invoke the factory, after all the logic for returning
216     * an existing Turnout has been invoked.
217     *
218     * @param systemName the system name to use for the new Turnout
219     * @param userName   the user name to use for the new Turnout
220     * @return the new Turnout or
221     * @throws IllegalArgumentException if unsuccessful
222     */
223    @Nonnull
224    abstract protected Turnout createNewTurnout(@Nonnull String systemName, String userName) throws IllegalArgumentException;
225
226    /** {@inheritDoc} */
227    @Override
228    @Nonnull
229    public String[] getValidOperationTypes() {
230        if (jmri.InstanceManager.getNullableDefault(jmri.CommandStation.class) != null) {
231            return new String[]{"Sensor", "Raw", "NoFeedback"};
232        } else {
233            return new String[]{"Sensor", "NoFeedback"};
234        }
235    }
236
237    /**
238     * Default Turnout ensures a numeric only system name.
239     * {@inheritDoc}
240     */
241    @Nonnull
242    @Override
243    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
244        return prefix + typeLetter() + checkNumeric(curAddress);
245    }
246
247    private String defaultClosedSpeed = "Normal";
248    private String defaultThrownSpeed = "Restricted";
249
250    /** {@inheritDoc} */
251    @Override
252    public void setDefaultClosedSpeed(@Nonnull String speed) throws JmriException {
253        Objects.requireNonNull(speed, "Value of requested turnout default closed speed can not be null");
254
255        if (defaultClosedSpeed.equals(speed)) {
256            return;
257        }
258        if (speed.contains("Block")) {
259            speed = "Block";
260            if (defaultClosedSpeed.equals(speed)) {
261                return;
262            }
263        } else {
264            try {
265                Float.parseFloat(speed);
266            } catch (NumberFormatException nx) {
267                try {
268                    jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
269                } catch (IllegalArgumentException ex) {
270                    throw new JmriException("Value of requested turnout default closed speed is not valid. " + ex.getMessage());
271                }
272            }
273        }
274        String oldSpeed = defaultClosedSpeed;
275        defaultClosedSpeed = speed;
276        firePropertyChange("DefaultTurnoutClosedSpeedChange", oldSpeed, speed);
277    }
278
279    /** {@inheritDoc} */
280    @Override
281    public void setDefaultThrownSpeed(@Nonnull String speed) throws JmriException {
282        Objects.requireNonNull(speed, "Value of requested turnout default thrown speed can not be null");
283
284        if (defaultThrownSpeed.equals(speed)) {
285            return;
286        }
287        if (speed.contains("Block")) {
288            speed = "Block";
289            if (defaultThrownSpeed.equals(speed)) {
290                return;
291            }
292
293        } else {
294            try {
295                Float.parseFloat(speed);
296            } catch (NumberFormatException nx) {
297                try {
298                    jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
299                } catch (IllegalArgumentException ex) {
300                    throw new JmriException("Value of requested turnout default thrown speed is not valid. " + ex.getMessage());
301                }
302            }
303        }
304        String oldSpeed = defaultThrownSpeed;
305        defaultThrownSpeed = speed;
306        firePropertyChange("DefaultTurnoutThrownSpeedChange", oldSpeed, speed);
307    }
308
309    /** {@inheritDoc} */
310    @Override
311    public String getDefaultThrownSpeed() {
312        return defaultThrownSpeed;
313    }
314
315    /** {@inheritDoc} */
316    @Override
317    public String getDefaultClosedSpeed() {
318        return defaultClosedSpeed;
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public String getEntryToolTip() {
324        return Bundle.getMessage("EnterNumber1to9999ToolTip");
325    }
326
327    private void handleIntervalChange(int newVal) {
328        turnoutInterval = newVal;
329        log.debug("in memo turnoutInterval changed to {}", turnoutInterval);
330    }
331
332    /** {@inheritDoc} */
333    @Override
334    public int getOutputInterval() {
335        return turnoutInterval;
336    }
337
338    /** {@inheritDoc} */
339    @Override
340    public void setOutputInterval(int newInterval) {
341        memo.setOutputInterval(newInterval);
342        turnoutInterval = newInterval; // local field will hear change and update automatically?
343        log.debug("turnoutInterval set to: {}", newInterval);
344    }
345
346    /**
347     * Duration in milliseconds of interval between separate Turnout commands on the same connection.
348     * <p>
349     * Change from e.g. XNetTurnout extensions and scripts using {@link #setOutputInterval(int)}
350     */
351    private int turnoutInterval = memo.getOutputInterval();
352    private LocalDateTime waitUntil = LocalDateTime.now();
353
354    /** {@inheritDoc} */
355    @Override
356    @Nonnull
357    public LocalDateTime outputIntervalEnds() {
358        log.debug("outputIntervalEnds called in AbstractTurnoutManager");
359        if (waitUntil.isAfter(LocalDateTime.now())) {
360            waitUntil = waitUntil.plus(turnoutInterval, ChronoUnit.MILLIS);
361        } else {
362            waitUntil = LocalDateTime.now().plus(turnoutInterval, ChronoUnit.MILLIS); // default interval = 250 Msec
363        }
364        return waitUntil;
365    }
366
367    /**
368     * Removes SensorManager and SystemConnectionMemo change listeners.
369     * {@inheritDoc}
370     */
371    @OverridingMethodsMustInvokeSuper
372    @Override
373    public void dispose(){
374        memo.removePropertyChangeListener(pcl);
375        InstanceManager.getDefault(SensorManager.class).removeVetoableChangeListener(this);
376        super.dispose();
377    }
378
379    private final static Logger log = LoggerFactory.getLogger(AbstractTurnoutManager.class);
380
381}