001package jmri.implementation;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.List;
006import javax.annotation.CheckForNull;
007import javax.annotation.Nonnull;
008import jmri.NamedBeanHandle;
009import jmri.Turnout;
010import jmri.util.ThreadingUtil;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * SignalMast implemented via a Binary Matrix (Truth Table) of Apects x Turnout objects.
016 * <p>
017 * A MatrixSignalMast is built up from an array of turnouts to control each aspect.
018 * System name specifies the creation information (except for the actual output beans):
019 * <pre>
020 * IF$xsm:basic:one-searchlight:($0001)-3t
021 * </pre> The name is a colon-separated series of terms:
022 * <ul>
023 * <li>IF$xsm - defines signal masts of this type (x for matri<b>X</b>)
024 * <li>basic - name of the signaling system
025 * <li>one-searchlight - name of the particular aspect map/mast model
026 * <li>($0001) - small ordinal number for telling various matrix signal masts apart
027 * <li>name ending in -nt for (binary) Turnout outputs
028 * where n = the number of binary outputs, between 1 and mastBitNum i.e. -3t</li>
029 * </ul>
030 *
031 * @author Bob Jacobsen Copyright (C) 2009, 2014, 2020
032 * @author Egbert Broerse Copyright (C) 2016, 2018, 2020
033 */
034public class MatrixSignalMast extends AbstractSignalMast {
035    /**
036     *  Number of columns in logix matrix, default to 6, set in Matrix Mast panel &amp; on loading xml.
037     *  Used to set size of char[] bitString.
038     *  See MAXMATRIXBITS in {@link jmri.jmrit.beantable.signalmast.MatrixSignalMastAddPane}.
039     */
040    private int mastBitNum = 6;
041    private int mDelay = 0;
042
043    private static final String errorChars = "nnnnnn";
044    private final char[] errorBits = errorChars.toCharArray();
045
046    private static final String emptyChars = "000000"; // default starting value
047    private final char[] emptyBits = emptyChars.toCharArray();
048
049    public MatrixSignalMast(String systemName, String userName) {
050        super(systemName, userName);
051        configureFromName(systemName);
052    }
053
054    public MatrixSignalMast(String systemName) {
055        super(systemName);
056        configureFromName(systemName);
057    }
058
059    private static final String THE_MAST_TYPE = "IF$xsm";
060
061    private void configureFromName(@Nonnull String systemName) {
062        // split out the basic information
063        String[] parts = systemName.split(":");
064        if (parts.length < 3) {
065            log.error("SignalMast system name needs at least three parts: {}", systemName);
066            throw new IllegalArgumentException("System name needs at least three parts: " + systemName);
067        }
068        if (!parts[0].equals(THE_MAST_TYPE)) {
069            log.warn("SignalMast system name should start with \"{}\" but is \"{}\"", THE_MAST_TYPE, systemName);
070        }
071        String system = parts[1];
072        String mast = parts[2];
073
074        mast = mast.substring(0, mast.indexOf("("));
075        setMastType(mast);
076
077        String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(")")); // retrieve ordinal from name
078        try {
079            int autoNumber = Integer.parseInt(tmp);
080            if (autoNumber > getLastRef()) {
081                setLastRef(autoNumber);
082            }
083        } catch (NumberFormatException e) {
084            log.warn("Auto generated SystemName \"{}\" is not in the correct format", systemName);
085        }
086
087        configureSignalSystemDefinition(system); // (checks for system) in AbstractSignalMast
088        configureAspectTable(system, mast); // (create -default- appmapping in var "map") in AbstractSignalMast
089    }
090
091    private final HashMap<String, char[]> aspectToOutput = new HashMap<>(16); // "Clear" - 01001 char[] pairs
092    private char[] unLitBits;
093
094    /**
095     * Store bits in aspectToOutput hashmap, synchronized.
096     * <p>
097     * Length of bitArray should match the number of outputs defined, so one digit per output.
098     *
099     * @param aspect String valid aspect to define
100     * @param bitArray char[] of on/off outputs for the aspect, like "00010"
101    */
102    public synchronized void setBitsForAspect(String aspect, char[] bitArray) {
103        if (aspectToOutput.containsKey(aspect)) {
104            if (log.isDebugEnabled()) log.debug("Aspect {} is already defined as {}", aspect, java.util.Arrays.toString(aspectToOutput.get(aspect)));
105            aspectToOutput.remove(aspect);
106        }
107        aspectToOutput.put(aspect, bitArray); // store keypair aspectname - bitArray in hashmap
108    }
109
110    /**
111     * Look up the pattern for an aspect.
112     *
113     * @param aspect String describing a (valid) signal mast aspect, like "Clear"
114     * only called for an already existing mast
115     * @return char[] of on/off outputs per aspect, like "00010"
116     * length of array should match the number of outputs defined
117     * when a mast is changed in the interface, extra 0's are added or superfluous elements deleted by the Add Mast panel
118    */
119    public synchronized char[] getBitsForAspect(String aspect) {
120        if (!aspectToOutput.containsKey(aspect) || aspectToOutput.get(aspect) == null) {
121            log.error("Trying to get aspect {} but it has not been configured", aspect);
122            return errorBits; // error flag
123        }
124        return aspectToOutput.get(aspect);
125    }
126
127    @Override
128    public void setAspect(@Nonnull String aspect) {
129        // check it's a valid choice
130        if (!map.checkAspect(aspect)) {
131            // not a valid aspect
132            log.warn("attempting to set invalid Aspect: {} on mast {}", aspect, getDisplayName());
133            throw new IllegalArgumentException("attempting to set invalid Aspect: " + aspect + " on mast: " + getDisplayName());
134        } else if (disabledAspects.contains(aspect)) {
135            log.warn("attempting to set an Aspect that has been Disabled: {} on mast {}", aspect, getDisplayName());
136            throw new IllegalArgumentException("attempting to set an Aspect that has been Disabled: " + aspect + " on mast: " + getDisplayName());
137        }
138        if (getLit()) {
139            synchronized (this) {
140                // If the signalmast is lit, then send the commands to change the aspect.
141                if (resetPreviousStates) {
142                    // Clear all the current states, this will result in the signalmast going "Stop" or unLit for a while
143                    if (aspectToOutput.containsKey("Stop")) {
144                        updateOutputs(getBitsForAspect("Stop")); // show Red
145                    } else {
146                        if (unLitBits != null) {
147                            updateOutputs(unLitBits); // Dark (instead of Red), always available
148                        }
149                    }
150                }
151                // add a timer here to wait a while before setting new aspect?
152                if (aspectToOutput.containsKey(aspect) && aspectToOutput.get(aspect) != errorBits) {
153                    char[] bitArray = getBitsForAspect(aspect);
154                    // for  MatrixMast nest a loop, using setBitsForAspect(), provides extra check on value
155                    updateOutputs(bitArray);
156                    // Set the new Signal Mast state
157                } else {
158                    log.error("Trying to set an aspect ({}) on signal mast {} which has not been configured", aspect, getDisplayName());
159                }
160            }
161        } else {
162            log.debug("Mast set to unlit, will not send aspect change to hardware");
163        }
164        super.setAspect(aspect);
165    }
166
167    @Override
168    public void setLit(boolean newLit) {
169        if (!allowUnLit() || newLit == getLit()) {
170            return;
171        }
172        super.setLit(newLit);
173        if (newLit) {
174            String litAspect = getAspect();
175            if (litAspect != null) {
176                setAspect(litAspect);
177            }
178            // if true, activate prior aspect
179        } else {
180            if (unLitBits != null) {
181                updateOutputs(unLitBits); // directly set outputs
182                //c.sendPacket(NmraPacket.altAccSignalDecoderPkt(dccSignalDecoderAddress, unLitId), packetRepeatCount);
183            }
184        }
185    }
186
187    public void setUnLitBits(@Nonnull char[] bits) {
188        unLitBits = bits;
189    }
190
191    /**
192     *  Receive unLitBits from xml and store.
193     *
194     *  @param bitString String for 1-n 1/0 chararacters setting an unlit aspect
195     */
196    public void setUnLitBits(@Nonnull String bitString) {
197        setUnLitBits(bitString.toCharArray());
198    }
199
200    /**
201     *  Provide Unlit bits to panel for editing.
202     *
203     *  @return char[] containing a series of 1's and 0's set for Unlit mast
204     */
205    @Nonnull public char[] getUnLitBits() {
206        if (unLitBits != null) {
207            return unLitBits;
208        } else {
209            return emptyBits;
210        }
211    }
212
213    /**
214     *  Hand unLitBits to xml.
215     *
216     *  @return String for 1-n 1/0 chararacters setting an unlit aspect
217     */
218    @Nonnull public String getUnLitChars() {
219        if (unLitBits != null) {
220            return String.valueOf(unLitBits);
221        } else {
222            log.error("Returning 0 values because unLitBits is empty");
223            return emptyChars.substring(0, (mastBitNum)); // should only be called when Unlit = true
224        }
225    }
226
227    /**
228     *  Fetch output as Turnout from outputsToBeans hashmap.
229     *
230     *  @param colNum int index (1 up to mastBitNum) for the column of the desired output
231     *  @return Turnout object connected to configured output
232     */
233    @CheckForNull private Turnout getOutputBean(int colNum) { // as bean
234        String key = "output" + colNum;
235        if (colNum > 0 && colNum <= outputsToBeans.size()) {
236            return outputsToBeans.get(key).getBean();
237        }
238        log.error("Trying to read bean for output {} which has not been configured", colNum);
239        return null;
240    }
241
242    /**
243     *  Fetch output from outputsToBeans hashmap.
244     *  Used?
245     *
246     *  @param colNum int index (1 up to mastBitNum) for the column of the desired output
247     *  @return NamedBeanHandle to the configured turnout output
248     */
249    @CheckForNull public NamedBeanHandle<Turnout> getOutputHandle(int colNum) {
250        String key = "output" + colNum;
251        if (colNum > 0 && colNum <= outputsToBeans.size()) {
252            return outputsToBeans.get(key);
253        }
254        log.error("Trying to read output NamedBeanHandle {} which has not been configured", key);
255        return null;
256    }
257
258    /**
259     *  Fetch output from outputsToBeans hashmap and provide to xml.
260     *
261     *  @see jmri.implementation.configurexml.MatrixSignalMastXml#store(java.lang.Object)
262     *  @param colnum int index (1 up to mastBitNum) for the column of the desired output
263     *  @return String with the desplay name of the configured turnout output
264     */
265    @Nonnull public String getOutputName(int colnum) {
266        String key = "output" + colnum;
267        if (colnum > 0 && colnum <= outputsToBeans.size()) {
268            return outputsToBeans.get(key).getName();
269        }
270        log.error("Trying to read name of output {} which has not been configured", colnum);
271        return "";
272    }
273
274    /**
275     *  Receive aspect name from xml and store matching setting in outputsToBeans hashmap.
276     *
277     *  @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
278     *  @param aspect String describing (valid) signal mast aspect, like "Clear"
279     *  @param bitString String of 1/0 digits representing on/off outputs per aspect, like "00010"
280     */
281    public synchronized void setBitstring(@Nonnull String aspect, @Nonnull String bitString) {
282        if (aspectToOutput.containsKey(aspect)) {
283            log.debug("Aspect {} is already defined so will override", aspect);
284            aspectToOutput.remove(aspect);
285        }
286        char[] bitArray = bitString.toCharArray(); // for faster lookup, stored as char[] array
287        aspectToOutput.put(aspect, bitArray);
288    }
289
290    /**
291     *  Receive aspect name from xml and store matching setting in outputsToBeans hashmap.
292     *
293     *  @param aspect String describing (valid) signal mast aspect, like "Clear"
294     *  @param bitArray char[] of 1/0 digits representing on/off outputs per aspect, like {0,0,0,1,0}
295     */
296    public synchronized void setBitstring(String aspect, char[] bitArray) {
297        if (aspectToOutput.containsKey(aspect)) {
298            log.debug("Aspect {} is already defined so will override", aspect);
299            aspectToOutput.remove(aspect);
300        }
301        // is supplied as char array, no conversion needed
302        aspectToOutput.put(aspect, bitArray);
303    }
304
305    /**
306     *  Provide one series of on/off digits from aspectToOutput hashmap to xml.
307     *
308     *  @return bitString String of 1 (= on) and 0 (= off) chars
309     *  @param aspect String describing valid signal mast aspect, like "Clear"
310     */
311    @Nonnull public synchronized String getBitstring(@Nonnull String aspect) {
312        if (aspectToOutput.containsKey(aspect)) { // hashtable
313            return new String(aspectToOutput.get(aspect)); // convert char[] to string
314        }
315        return "";
316    }
317
318    /**
319     *  Provide the names of the on/off turnout outputs from outputsToBeans hashmap to xml.
320     *
321     *  @return outputlist List&lt;String&gt; of display names for the outputs in order 1 to (max) mastBitNum
322     */
323    @Nonnull public List<String> getOutputs() { // provide to xml
324        // to do: use for loop
325        ArrayList<String> outputlist = new ArrayList<>();
326        //list = outputsToBeans.keySet();
327
328        int index = 1;
329        while (outputsToBeans.containsKey("output" + index)) {
330            outputlist.add(outputsToBeans.get("output" + index).getName());
331            index++;
332        }
333        return outputlist;
334    }
335
336    protected HashMap<String, NamedBeanHandle<Turnout>> outputsToBeans = new HashMap<>(); // output# - bean pairs
337
338    /**
339     * Receive properties from xml, convert name to NamedBeanHandle, store in hashmap outputsToBeans.
340     *
341     * @param colname String describing the name of the corresponding output, like "output1"
342     * @param turnoutname String for the display name of the output, like "LT1"
343     */
344    public void setOutput(@Nonnull String colname, @Nonnull String turnoutname) {
345        Turnout turn = jmri.InstanceManager.turnoutManagerInstance().getTurnout(turnoutname);
346        if (turn == null) {
347            log.error("setOutput couldn't locate turnout {}", turnoutname);
348            return;
349        }
350        NamedBeanHandle<Turnout> namedTurnout = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(turnoutname, turn);
351        if (outputsToBeans.containsKey(colname)) {
352            log.debug("Output {} is already defined so will override", colname);
353            outputsToBeans.remove(colname);
354        }
355        outputsToBeans.put(colname, namedTurnout);
356    }
357
358    /**
359     *  Send hardware instruction.
360     *
361     *  @param bits char[] of on/off outputs per aspect, like "00010"
362     *  Length of array should match the number of outputs defined
363     */
364    public void updateOutputs(char[] bits) {
365        int newState;
366        if (bits == null){
367            log.debug("Empty char[] received");
368        } else {
369            for (int i = 0; i < outputsToBeans.size(); i++) {
370                log.debug("Setting bits[1] = {} for output #{}", bits[i], i);
371                Turnout t = getOutputBean(i + 1);
372                if (t != null) {
373                    t.setBinaryOutput(true); // prevent feedback etc.
374                }
375                if (bits[i] == '1' && t != null && t.getCommandedState() != Turnout.CLOSED) {
376                    // no need to set a state already set
377                    newState = Turnout.CLOSED;
378                } else if (bits[i] == '0' && t != null && t.getCommandedState() != Turnout.THROWN) {
379                    newState = Turnout.THROWN;
380                } else if (bits[i] == 'n' || bits[i] == 'u') {
381                    // let pass, extra chars up to mastBitNum are not defined
382                    newState = -1;
383                } else {
384                    // invalid char or state is already set
385                    newState = -2;
386                    log.debug("Element {} not converted to state for output #{}", bits[i], i);
387                }
388                // wait mast specific delay before sending each (valid) state change to a (valid) output
389                if (newState >= 0 && t != null) { // t!=null check required
390                    final int toState = newState;
391                    final Turnout setTurnout = t;
392                    ThreadingUtil.runOnLayoutEventually(() -> {   // eventually, even though we have timing here, should be soon
393                        setTurnout.setCommandedStateAtInterval(toState); // delayed on specific connection by its turnoutManager
394                    });
395                    try {
396                        Thread.sleep(mDelay); // only the Mast specific user defined delay is applied here
397                    } catch (InterruptedException e) {
398                        log.debug("interrupted in updateOutputs");
399                        Thread.currentThread().interrupt(); // retain if needed later
400                        return;
401                    }
402                }
403            }
404        }
405    }
406
407    private boolean resetPreviousStates = false;
408
409    /**
410     * If the signal mast driver requires the previous state to be cleared down
411     * before the next state is set.
412     *
413     * @param boo true to configure for intermediate reset step
414     */
415    public void resetPreviousStates(boolean boo) {
416        resetPreviousStates = boo;
417    }
418
419    public boolean resetPreviousStates() {
420        return resetPreviousStates;
421    }
422
423/*    Turnout getTurnoutBean(int i) { // as bean
424        String key = "output" + Integer.toString(i);
425        if (i < 1 || i > outputsToBeans.size() ) {
426            return null;
427        }
428        if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null){
429            return outputsToBeans.get(key).getBean();
430        }
431        return null;
432    }*/
433
434/*    public String getTurnoutName(int i) {
435        String key = "output" + Integer.toString(i);
436        if (i < 1 || i > outputsToBeans.size() ) {
437            return null;
438        }
439        if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null) {
440            return outputsToBeans.get(key).getName();
441        }
442        return null;
443    }*/
444
445    boolean isTurnoutUsed(Turnout t) {
446        for (int i = 1; i <= outputsToBeans.size(); i++) {
447            if (t.equals(getOutputBean(i))) {
448                return true;
449            }
450        }
451        return false;
452    }
453
454    /**
455     * @return highest ordinal of all MatrixSignalMasts in use
456     */
457    public static int getLastRef() {
458        return lastRef;
459    }
460
461    /**
462     *
463     * @param newVal for ordinal of all MatrixSignalMasts in use
464     */
465    protected static void setLastRef(int newVal) {
466        lastRef = newVal;
467    }
468
469    /**
470     * Ordinal of all MatrixSignalMasts to create unique system name.
471     */
472    private static volatile int lastRef = 0;
473
474    @Override
475    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
476        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
477            if (evt.getOldValue() instanceof Turnout) {
478                if (isTurnoutUsed((Turnout) evt.getOldValue())) {
479                    java.beans.PropertyChangeEvent e = new java.beans.PropertyChangeEvent(this, "DoNotDelete", null, null);
480                    throw new java.beans.PropertyVetoException(Bundle.getMessage("InUseTurnoutSignalMastVeto", getDisplayName()), e);
481                }
482            }
483        }
484    }
485
486    /**
487     * Store number of outputs from integer.
488     *
489     * @param number int for the number of outputs defined for this mast
490     * @see #mastBitNum
491     */
492    public void setBitNum(int number) {
493            mastBitNum = number;
494    }
495
496    /**
497     * Store number of outputs from integer.
498     *
499     * @param bits char[] for outputs defined for this mast
500     * @see #mastBitNum
501     */
502    public void setBitNum(char[] bits) {
503        mastBitNum = bits.length;
504    }
505
506    public int getBitNum() {
507        return mastBitNum;
508    }
509
510    @Override
511    public void setAspectDisabled(String aspect) {
512        if (aspect == null || aspect.equals("")) {
513            return;
514        }
515        if (!map.checkAspect(aspect)) {
516            log.warn("attempting to disable an aspect: {} that is not on mast {}", aspect, getDisplayName());
517            return;
518        }
519        if (!disabledAspects.contains(aspect)) {
520            disabledAspects.add(aspect);
521            firePropertyChange("aspectDisabled", null, aspect);
522        }
523    }
524
525    /**
526     * Set the delay between issuing Matrix Output commands to the outputs on this specific mast.
527     * Mast Delay will be extended by a connection specific Output Delay set in the connection config.
528     *
529     * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
530     * @param delay the new delay in milliseconds
531     */
532    public void setMatrixMastCommandDelay(int delay) {
533        if (delay >= 0) {
534            mDelay = delay;
535        }
536    }
537
538    /**
539     * Get the delay between issuing Matrix Output commands to the outputs on this specific mast.
540     * Delay be extended by a connection specific Output Delay set in the connection config.
541     *
542     * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
543     * @return the delay in milliseconds
544     */
545    public int getMatrixMastCommandDelay() {
546        return mDelay;
547    }
548
549    private final static Logger log = LoggerFactory.getLogger(MatrixSignalMast.class);
550
551}