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