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 mastType = "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(mastType)) {
069            log.warn("SignalMast system name should start with \"{}\" but is \"{}\"", mastType, 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            if (getAspect() != null) {
175                setAspect(getAspect());
176            }
177            // if true, activate prior aspect
178        } else {
179            if (unLitBits != null) {
180                updateOutputs(unLitBits); // directly set outputs
181                //c.sendPacket(NmraPacket.altAccSignalDecoderPkt(dccSignalDecoderAddress, unLitId), packetRepeatCount);
182            }
183        }
184    }
185
186    public void setUnLitBits(@Nonnull char[] bits) {
187        unLitBits = bits;
188    }
189
190    /**
191     *  Receive unLitBits from xml and store.
192     *
193     *  @param bitString String for 1-n 1/0 chararacters setting an unlit aspect
194     */
195    public void setUnLitBits(@Nonnull String bitString) {
196        setUnLitBits(bitString.toCharArray());
197    }
198
199    /**
200     *  Provide Unlit bits to panel for editing.
201     *
202     *  @return char[] containing a series of 1's and 0's set for Unlit mast
203     */
204    @Nonnull public char[] getUnLitBits() {
205        if (unLitBits != null) {
206            return unLitBits;
207        } else {
208            return emptyBits;
209        }
210    }
211
212    /**
213     *  Hand unLitBits to xml.
214     *
215     *  @return String for 1-n 1/0 chararacters setting an unlit aspect
216     */
217    @Nonnull public String getUnLitChars() {
218        if (unLitBits != null) {
219            return String.valueOf(unLitBits);
220        } else {
221            log.error("Returning 0 values because unLitBits is empty");
222            return emptyChars.substring(0, (mastBitNum)); // should only be called when Unlit = true
223        }
224    }
225
226    /**
227     *  Fetch output as Turnout from outputsToBeans hashmap.
228     *
229     *  @param colNum int index (1 up to mastBitNum) for the column of the desired output
230     *  @return Turnout object connected to configured output
231     */
232    @CheckForNull private Turnout getOutputBean(int colNum) { // as bean
233        String key = "output" + colNum;
234        if (colNum > 0 && colNum <= outputsToBeans.size()) {
235            return outputsToBeans.get(key).getBean();
236        }
237        log.error("Trying to read bean for output {} which has not been configured", colNum);
238        return null;
239    }
240
241    /**
242     *  Fetch output from outputsToBeans hashmap.
243     *  Used?
244     *
245     *  @param colNum int index (1 up to mastBitNum) for the column of the desired output
246     *  @return NamedBeanHandle to the configured turnout output
247     */
248    @CheckForNull public NamedBeanHandle<Turnout> getOutputHandle(int colNum) {
249        String key = "output" + colNum;
250        if (colNum > 0 && colNum <= outputsToBeans.size()) {
251            return outputsToBeans.get(key);
252        }
253        log.error("Trying to read output NamedBeanHandle {} which has not been configured", key);
254        return null;
255    }
256
257    /**
258     *  Fetch output from outputsToBeans hashmap and provide to xml.
259     *
260     *  @see jmri.implementation.configurexml.MatrixSignalMastXml#store(java.lang.Object)
261     *  @param colnum int index (1 up to mastBitNum) for the column of the desired output
262     *  @return String with the desplay name of the configured turnout output
263     */
264    @Nonnull public String getOutputName(int colnum) {
265        String key = "output" + colnum;
266        if (colnum > 0 && colnum <= outputsToBeans.size()) {
267            return outputsToBeans.get(key).getName();
268        }
269        log.error("Trying to read name of output {} which has not been configured", colnum);
270        return "";
271    }
272
273    /**
274     *  Receive aspect name from xml and store matching setting in outputsToBeans hashmap.
275     *
276     *  @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
277     *  @param aspect String describing (valid) signal mast aspect, like "Clear"
278     *  @param bitString String of 1/0 digits representing on/off outputs per aspect, like "00010"
279     */
280    public synchronized void setBitstring(@Nonnull String aspect, @Nonnull String bitString) {
281        if (aspectToOutput.containsKey(aspect)) {
282            log.debug("Aspect {} is already defined so will override", aspect);
283            aspectToOutput.remove(aspect);
284        }
285        char[] bitArray = bitString.toCharArray(); // for faster lookup, stored as char[] array
286        aspectToOutput.put(aspect, bitArray);
287    }
288
289    /**
290     *  Receive aspect name from xml and store matching setting in outputsToBeans hashmap.
291     *
292     *  @param aspect String describing (valid) signal mast aspect, like "Clear"
293     *  @param bitArray char[] of 1/0 digits representing on/off outputs per aspect, like {0,0,0,1,0}
294     */
295    public synchronized void setBitstring(String aspect, char[] bitArray) {
296        if (aspectToOutput.containsKey(aspect)) {
297            log.debug("Aspect {} is already defined so will override", aspect);
298            aspectToOutput.remove(aspect);
299        }
300        // is supplied as char array, no conversion needed
301        aspectToOutput.put(aspect, bitArray);
302    }
303
304    /**
305     *  Provide one series of on/off digits from aspectToOutput hashmap to xml.
306     *
307     *  @return bitString String of 1 (= on) and 0 (= off) chars
308     *  @param aspect String describing valid signal mast aspect, like "Clear"
309     */
310    @Nonnull public synchronized String getBitstring(@Nonnull String aspect) {
311        if (aspectToOutput.containsKey(aspect)) { // hashtable
312            return new String(aspectToOutput.get(aspect)); // convert char[] to string
313        }
314        return "";
315    }
316
317    /**
318     *  Provide the names of the on/off turnout outputs from outputsToBeans hashmap to xml.
319     *
320     *  @return outputlist List&lt;String&gt; of display names for the outputs in order 1 to (max) mastBitNum
321     */
322    @Nonnull public List<String> getOutputs() { // provide to xml
323        // to do: use for loop
324        ArrayList<String> outputlist = new ArrayList<>();
325        //list = outputsToBeans.keySet();
326        
327        int index = 1;
328        while (outputsToBeans.containsKey("output" + index)) {
329            outputlist.add(outputsToBeans.get("output" + index).getName());
330            index++;
331        }
332        return outputlist;
333    }
334
335    protected HashMap<String, NamedBeanHandle<Turnout>> outputsToBeans = new HashMap<>(); // output# - bean pairs
336
337    /**
338     * Receive properties from xml, convert name to NamedBeanHandle, store in hashmap outputsToBeans.
339     *
340     * @param colname String describing the name of the corresponding output, like "output1"
341     * @param turnoutname String for the display name of the output, like "LT1"
342     */
343    public void setOutput(@Nonnull String colname, @Nonnull String turnoutname) {
344        Turnout turn = jmri.InstanceManager.turnoutManagerInstance().getTurnout(turnoutname);
345        if (turn == null) {
346            log.error("setOutput couldn't locate turnout {}", turnoutname);
347            return;
348        }
349        NamedBeanHandle<Turnout> namedTurnout = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(turnoutname, turn);
350        if (outputsToBeans.containsKey(colname)) {
351            log.debug("Output {} is already defined so will override", colname);
352            outputsToBeans.remove(colname);
353        }
354        outputsToBeans.put(colname, namedTurnout);
355    }
356
357    /**
358     *  Send hardware instruction.
359     *
360     *  @param bits char[] of on/off outputs per aspect, like "00010"
361     *  Length of array should match the number of outputs defined
362     */
363    public void updateOutputs(char[] bits) {
364        int newState;
365        if (bits == null){
366            log.debug("Empty char[] received");
367        } else {
368            for (int i = 0; i < outputsToBeans.size(); i++) {
369                log.debug("Setting bits[1] = {} for output #{}", bits[i], i);
370                Turnout t = getOutputBean(i + 1);
371                if (t != null) {
372                    t.setBinaryOutput(true); // prevent feedback etc.
373                }
374                if (bits[i] == '1' && t != null && t.getCommandedState() != Turnout.CLOSED) {
375                    // no need to set a state already set
376                    newState = Turnout.CLOSED;
377                } else if (bits[i] == '0' && t != null && t.getCommandedState() != Turnout.THROWN) {
378                    newState = Turnout.THROWN;
379                } else if (bits[i] == 'n' || bits[i] == 'u') {
380                    // let pass, extra chars up to mastBitNum are not defined
381                    newState = -1;
382                } else {
383                    // invalid char or state is already set
384                    newState = -2;
385                    log.debug("Element {} not converted to state for output #{}", bits[i], i);
386                }
387                // wait mast specific delay before sending each (valid) state change to a (valid) output
388                if (newState >= 0 && t != null) { // t!=null check required
389                    final int toState = newState;
390                    final Turnout setTurnout = t;
391                    ThreadingUtil.runOnLayoutEventually(() -> {   // eventually, even though we have timing here, should be soon
392                        setTurnout.setCommandedStateAtInterval(toState); // delayed on specific connection by its turnoutManager
393                    });
394                    try {
395                        Thread.sleep(mDelay); // only the Mast specific user defined delay is applied here
396                    } catch (InterruptedException e) {
397                        Thread.currentThread().interrupt(); // retain if needed later
398                    }
399                }
400            }
401        }
402    }
403
404    private boolean resetPreviousStates = false;
405
406    /**
407     * If the signal mast driver requires the previous state to be cleared down
408     * before the next state is set.
409     *
410     * @param boo true to configure for intermediate reset step
411     */
412    public void resetPreviousStates(boolean boo) {
413        resetPreviousStates = boo;
414    }
415
416    public boolean resetPreviousStates() {
417        return resetPreviousStates;
418    }
419
420/*    Turnout getTurnoutBean(int i) { // as bean
421        String key = "output" + Integer.toString(i);
422        if (i < 1 || i > outputsToBeans.size() ) {
423            return null;
424        }
425        if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null){
426            return outputsToBeans.get(key).getBean();
427        }
428        return null;
429    }*/
430
431/*    public String getTurnoutName(int i) {
432        String key = "output" + Integer.toString(i);
433        if (i < 1 || i > outputsToBeans.size() ) {
434            return null;
435        }
436        if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null) {
437            return outputsToBeans.get(key).getName();
438        }
439        return null;
440    }*/
441
442    boolean isTurnoutUsed(Turnout t) {
443        for (int i = 1; i <= outputsToBeans.size(); i++) {
444            if (t.equals(getOutputBean(i))) {
445                return true;
446            }
447        }
448        return false;
449    }
450
451    /**
452     * @return highest ordinal of all MatrixSignalMasts in use
453     */
454    public static int getLastRef() {
455        return lastRef;
456    }
457
458    /**
459     *
460     * @param newVal for ordinal of all MatrixSignalMasts in use
461     */
462    protected static void setLastRef(int newVal) {
463        lastRef = newVal;
464    }
465
466    /**
467     * Ordinal of all MatrixSignalMasts to create unique system name.
468     */
469    private static volatile int lastRef = 0;
470
471    @Override
472    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
473        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
474            if (evt.getOldValue() instanceof Turnout) {
475                if (isTurnoutUsed((Turnout) evt.getOldValue())) {
476                    java.beans.PropertyChangeEvent e = new java.beans.PropertyChangeEvent(this, "DoNotDelete", null, null);
477                    throw new java.beans.PropertyVetoException(Bundle.getMessage("InUseTurnoutSignalMastVeto", getDisplayName()), e);
478                }
479            }
480        }
481    }
482
483    /**
484     * Store number of outputs from integer.
485     *
486     * @param number int for the number of outputs defined for this mast
487     * @see #mastBitNum
488     */
489    public void setBitNum(int number) {
490            mastBitNum = number;
491    }
492
493    /**
494     * Store number of outputs from integer.
495     *
496     * @param bits char[] for outputs defined for this mast
497     * @see #mastBitNum
498     */
499    public void setBitNum(char[] bits) {
500        mastBitNum = bits.length;
501    }
502
503    public int getBitNum() {
504        return mastBitNum;
505    }
506
507    @Override
508    public void setAspectDisabled(String aspect) {
509        if (aspect == null || aspect.equals("")) {
510            return;
511        }
512        if (!map.checkAspect(aspect)) {
513            log.warn("attempting to disable an aspect: {} that is not on mast {}", aspect, getDisplayName());
514            return;
515        }
516        if (!disabledAspects.contains(aspect)) {
517            disabledAspects.add(aspect);
518            firePropertyChange("aspectDisabled", null, aspect);
519        }
520    }
521
522    /**
523     * Set the delay between issuing Matrix Output commands to the outputs on this specific mast.
524     * Mast Delay will be extended by a connection specific Output Delay set in the connection config.
525     *
526     * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
527     * @param delay the new delay in milliseconds
528     */
529    public void setMatrixMastCommandDelay(int delay) {
530        if (delay >= 0) {
531            mDelay = delay;
532        }
533    }
534
535    /**
536     * Get the delay between issuing Matrix Output commands to the outputs on this specific mast.
537     * Delay be extended by a connection specific Output Delay set in the connection config.
538     *
539     * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element)
540     * @return the delay in milliseconds
541     */
542    public int getMatrixMastCommandDelay() {
543        return mDelay;
544    }
545
546    @Override
547    public void dispose() {
548        super.dispose();
549    }
550
551    private final static Logger log = LoggerFactory.getLogger(MatrixSignalMast.class);
552
553}