001package jmri.jmrit.beantable.signalmast;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.text.DecimalFormat;
006import java.util.*;
007import javax.annotation.Nonnull;
008import javax.swing.*;
009import javax.swing.border.TitledBorder;
010import jmri.*;
011import jmri.implementation.*;
012import jmri.util.*;
013import jmri.util.swing.*;
014
015import org.openide.util.lookup.ServiceProvider;
016
017/**
018 * A pane for configuring MatrixSignalMast objects.
019 *
020 * @see jmri.jmrit.beantable.signalmast.SignalMastAddPane
021 * @author Bob Jacobsen Copyright (C) 2018
022 * @author Egbert Broerse Copyright (C) 2016, 2019
023 * @since 4.11.2
024 */
025public class MatrixSignalMastAddPane extends SignalMastAddPane {
026
027    public MatrixSignalMastAddPane() {
028        init();
029    }
030    
031    final void init() {
032        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
033        // lit/unlit controls
034        JPanel p = new JPanel();
035        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
036        p.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("AllowUnLitLabel"))));
037        p.add(allowUnLit);
038        p.setAlignmentX(Component.LEFT_ALIGNMENT);
039        add(p);
040
041        add(matrixUnLitPanel);
042        matrixUnLitPanel();
043        
044        matrixMastBitnumPanel = makeMatrixMastBitnumPanel(); // create panel
045        add(matrixMastBitnumPanel);
046        if (prefs.getComboBoxLastSelection(matrixBitNumSelectionCombo) != null) {
047            columnChoice.setSelectedItem(prefs.getComboBoxLastSelection(matrixBitNumSelectionCombo)); // setting for bitNum
048        }
049
050        matrixMastScroll = new JScrollPane(matrixMastPanel);
051        matrixMastScroll.setBorder(BorderFactory.createEmptyBorder());
052        add(matrixMastScroll);
053    }
054
055    private DefaultSignalAppearanceMap map;
056    private MatrixSignalMast currentMast; // mast being edited, null for new mast
057    private JCheckBox resetPreviousState = new JCheckBox(Bundle.getMessage("ResetPrevious"));
058    private UserPreferencesManager prefs = InstanceManager.getDefault(UserPreferencesManager.class);
059    private String matrixBitNumSelectionCombo = this.getClass().getName() + ".matrixBitNumSelected";
060    private JCheckBox allowUnLit = new JCheckBox();
061
062    private JScrollPane matrixMastScroll;
063    private JPanel matrixMastBitnumPanel;
064    private JPanel matrixMastPanel = new JPanel();
065    // private char[] bitString;
066    private char[] unLitPanelBits;
067    private int numberOfActiveAspects;
068
069    private JLabel bitNumLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("MatrixBitsLabel")));
070    private JComboBox<String> columnChoice = new JComboBox<>(choiceArray());
071    private JSpinner timeDelay = new JSpinner();
072
073    private LinkedHashMap<String, MatrixAspectPanel> matrixAspect = new LinkedHashMap<>(NOTIONAL_ASPECT_COUNT);
074    // LinkedHM type keeps things sorted // only used once, see updateMatrixAspectPanel()
075
076    private DecimalFormat paddedNumber = new DecimalFormat("0000");
077
078    private String[] choiceArray() {
079        String[] numberOfOutputs = new String[MAXMATRIXBITS];
080        for (int i = 0; i < MAXMATRIXBITS; i++) {
081            numberOfOutputs[i] = (i + 1) + "";
082        }
083        log.debug("Created output combo box: {}", Arrays.toString(numberOfOutputs));
084        return numberOfOutputs;
085    }
086
087    /**
088     * Set the maximum number of outputs for Matrix Signal Masts.
089     * Used in combobox and for loops.
090     */
091    public static final int MAXMATRIXBITS = 10; // requires code changes to modify this value
092
093    private String emptyChars = "0000000000"; // size of String = MAXMATRIXBITS; extend if MAXMATRIXBITS changed
094    private char[] emptyBits = emptyChars.toCharArray();
095
096    /**
097     * on = thrown, off = closed, no turnout states asked
098     */
099    private BeanSelectCreatePanel<Turnout> turnoutBox1 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
100    private BeanSelectCreatePanel<Turnout> turnoutBox2 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
101    private BeanSelectCreatePanel<Turnout> turnoutBox3 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
102    private BeanSelectCreatePanel<Turnout> turnoutBox4 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
103    private BeanSelectCreatePanel<Turnout> turnoutBox5 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
104    private BeanSelectCreatePanel<Turnout> turnoutBox6 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
105    private BeanSelectCreatePanel<Turnout> turnoutBox7 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
106    private BeanSelectCreatePanel<Turnout> turnoutBox8 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
107    private BeanSelectCreatePanel<Turnout> turnoutBox9 = new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
108    private BeanSelectCreatePanel<Turnout> turnoutBox10= new BeanSelectCreatePanel<>(InstanceManager.turnoutManagerInstance(), null);
109    // repeat in order to extend MAXMATRIXBITS 
110
111    /**
112     * The number of columns in logic matrix. 
113     * This is set when the user resizes the mast
114     */
115    private int bitNum;
116    
117    // ToDo: add boxes to set DCC Packets (with drop down selection "Output Type": Turnouts/Direct DCC Packets)
118
119    /** {@inheritDoc} */
120    @Override
121    @Nonnull public String getPaneName() {
122        return Bundle.getMessage("MatrixCtlMast");
123    }
124
125    /** {@inheritDoc} */
126    @Override
127    public void setAspectNames(@Nonnull SignalAppearanceMap newMap, @Nonnull SignalSystem sigSystem) {
128        log.debug("setAspectNames(...)");
129
130        unLitPanelBits = Arrays.copyOf(emptyBits, MAXMATRIXBITS);
131        map = (DefaultSignalAppearanceMap)newMap;
132
133        // set up rest of panel
134        updateMatrixMastPanel(); // show only the correct amount of columns for existing matrixMast
135
136        columnChoice.setSelectedIndex(bitNum - 1); // index of items in list starts counting at 0 while "1" is displayed
137    }
138
139    /** {@inheritDoc} */
140    @Override
141    public boolean canHandleMast(@Nonnull SignalMast mast) {
142        return mast instanceof MatrixSignalMast;
143    }
144
145    /** {@inheritDoc} */
146    @Override
147    public void setMast(SignalMast mast) { 
148        log.debug("setMast({})", mast);
149        if (mast == null) { 
150            currentMast = null;
151            return; 
152        }
153        
154        if (! (mast instanceof MatrixSignalMast) ) {
155            log.error("mast was wrong type: {} {}", mast.getSystemName(), mast.getClass().getName());
156            return;
157        }
158
159        currentMast = (MatrixSignalMast) mast;
160
161        bitNum = currentMast.getBitNum(); // number of matrix columns = logic outputs = number of bits per Aspect
162        updateMatrixMastPanel(); // show only the correct amount of columns for existing matrixMast
163        // @see copyFromAnotherMatrixMastAspect(mast)
164        if (map != null) {
165            Enumeration<String> aspects = map.getAspects();
166            // in matrixPanel LinkedHashtable, fill in mast settings per aspect
167            while (aspects.hasMoreElements()) {
168                String key = aspects.nextElement(); // for each aspect
169                MatrixAspectPanel matrixPanel = matrixAspect.get(key); // load aspectpanel from hashmap
170                matrixPanel.setAspectDisabled(currentMast.isAspectDisabled(key)); // sets a disabled aspect
171                if ( ! currentMast.isAspectDisabled(key)) { // bits not saved in mast when disabled, so we should not load them back in
172                    char[] mastBits = currentMast.getBitsForAspect(key); // same as loading an existing MatrixMast
173                    char[] panelAspectBits = Arrays.copyOf(mastBits, MAXMATRIXBITS); // store as [6] character array in panel
174                    matrixPanel.updateAspectBits(panelAspectBits);
175                    matrixPanel.setAspectBoxes(panelAspectBits);
176                    // sets boxes 1 - MAXMATRIXBITS on aspect sub panel from values in hashmap char[] like: 1001
177                }
178            }
179        }
180
181        columnChoice.setSelectedIndex(bitNum - 1); // index of items in list starts counting at 0 while "1" is displayed
182        columnChoice.setEnabled(false);
183        // fill in the names of the outputs from mast:
184        if ( !currentMast.getOutputName(1).isEmpty()) {
185            turnoutBox1.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(1))); // load input into turnoutBox1
186        }
187        if (bitNum > 1 && !currentMast.getOutputName(2).isEmpty()) {
188            turnoutBox2.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(2))); // load input into turnoutBox2
189        }
190        if (bitNum > 2 && !currentMast.getOutputName(3).isEmpty()) {
191            turnoutBox3.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(3))); // load input into turnoutBox3
192        }
193        if (bitNum > 3 && !currentMast.getOutputName(4).isEmpty()) {
194            turnoutBox4.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(4))); // load input into turnoutBox4
195        }
196        if (bitNum > 4 && !currentMast.getOutputName(5).isEmpty()) {
197            turnoutBox5.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(5))); // load input into turnoutBox5
198        }
199        if (bitNum > 5 && !currentMast.getOutputName(6).isEmpty()) {
200            turnoutBox6.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(6))); // load input into turnoutBox6
201        }
202        if (bitNum > 6 && !currentMast.getOutputName(6).isEmpty()) {
203            turnoutBox7.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(7))); // load input into turnoutBox6
204        }
205        if (bitNum > 7 && !currentMast.getOutputName(6).isEmpty()) {
206            turnoutBox8.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(8))); // load input into turnoutBox6
207        }
208        if (bitNum > 8 && !currentMast.getOutputName(6).isEmpty()) {
209            turnoutBox9.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(9))); // load input into turnoutBox6
210        }
211        if (bitNum > 9 && !currentMast.getOutputName(6).isEmpty()) {
212            turnoutBox10.setDefaultNamedBean(InstanceManager.turnoutManagerInstance().getTurnout(currentMast.getOutputName(10))); // load input into turnoutBox6
213        }
214        // repeat in order to extend MAXMATRIXBITS
215        if (currentMast.resetPreviousStates()) {
216            resetPreviousState.setSelected(true);
217        }
218 
219        unLitPanelBits = Arrays.copyOf(currentMast.getUnLitBits(), MAXMATRIXBITS); // store as MAXMATRIXBITS character array
220        
221        // transfer the lit/unlit status to checkboxes
222        for (int i = 0; i < MAXMATRIXBITS; i++) {
223            unlitCheckArray[i].setSelected(unLitPanelBits[i] == '1');
224        }
225        
226        allowUnLit.setSelected(currentMast.allowUnLit());
227        // set up additional mast specific Delay
228        timeDelay.setValue(currentMast.getMatrixMastCommandDelay());
229
230        log.trace("setMast {} end", mast);
231    }
232
233    /** {@inheritDoc} */
234    @Override
235    public boolean createMast(@Nonnull String sigsysname, @Nonnull String mastname, @Nonnull String username) {
236        log.debug("createMast({},{})", sigsysname, mastname);
237        String newMastName; // UserName for mast
238
239        // check all output boxes are filled (calling getDisplayName() would immediately create a new bean (with an empty comment)
240        if (       (              ( turnoutBox1.isEmpty() ) )
241                || (bitNum > 1 && ( turnoutBox2.isEmpty() ) )
242                || (bitNum > 2 && ( turnoutBox3.isEmpty() ) )
243                || (bitNum > 3 && ( turnoutBox4.isEmpty() ) )
244                || (bitNum > 4 && ( turnoutBox5.isEmpty() ) )
245                || (bitNum > 5 && ( turnoutBox6.isEmpty() ) )
246                || (bitNum > 6 && ( turnoutBox7.isEmpty() ) )
247                || (bitNum > 7 && ( turnoutBox8.isEmpty() ) )
248                || (bitNum > 8 && ( turnoutBox9.isEmpty() ) )
249                || (bitNum > 9 && ( turnoutBox10.isEmpty() ) )
250        ) {
251            // add an extra OR in list above in order to extend MAXMATRIXBITS
252            // error dialog
253            JOptionPane.showMessageDialog(null, Bundle.getMessage("MatrixOutputEmpty", mastname),
254                    Bundle.getMessage("WarningTitle"),
255                    JOptionPane.ERROR_MESSAGE);
256            log.warn("Empty output on panel");
257            return false;
258        }
259
260        // check/warn if bit sets are identical
261        if (identicalBits()) {
262            // error dialog
263            JOptionPane.showMessageDialog(null, Bundle.getMessage("AspectMastBitsWarning", (int) Math.sqrt(numberOfActiveAspects), numberOfActiveAspects),
264                    Bundle.getMessage("WarningTitle"),
265                    JOptionPane.ERROR_MESSAGE);
266            log.warn("Identical bits on panel");
267            return false;
268        }
269
270        if (currentMast == null) {
271            // Create was pressed for new mast: create new MatrixMast with props from panel
272            newMastName = "IF$xsm:"
273                    + sigsysname
274                    + ":" + mastname.substring(11, mastname.length() - 4);
275            newMastName += "($" + (paddedNumber.format(MatrixSignalMast.getLastRef() + 1));
276            newMastName += ")" + "-" + bitNum + "t"; // for the number of t = "turnout-outputs"
277            // TODO: add d = option for direct packets
278            currentMast = new MatrixSignalMast(newMastName); // timedDelay is stored later on
279            InstanceManager.getDefault(SignalMastManager.class).register(currentMast);
280        } else {
281            newMastName = currentMast.getSystemName();
282        }
283
284        currentMast.setBitNum(bitNum); // store number of columns in aspect - outputs matrix in mast
285
286        // store outputs from turnoutBoxes; see method MatrixSignalMast#setOutput(colname, turnoutname) line 357
287        log.debug("newMastName = {}", newMastName);
288        currentMast.setOutput("output1", turnoutBox1.getDisplayName()); // store choice from turnoutBox1, creates bean if needed
289        try {
290            turnoutBox1.updateComment(turnoutBox1.getNamedBean(), newMastName + ":output1"); // write mast name to output1 bean comment
291            if (bitNum > 1) {
292                currentMast.setOutput("output2", turnoutBox2.getDisplayName()); // store choice from turnoutBox2
293                turnoutBox2.updateComment(turnoutBox2.getNamedBean(), newMastName + ":output2");
294                if (bitNum > 2) {
295                    currentMast.setOutput("output3", turnoutBox3.getDisplayName()); // store choice from turnoutBox3
296                    turnoutBox3.updateComment(turnoutBox3.getNamedBean(), newMastName + ":output3");
297                    if (bitNum > 3) {
298                        currentMast.setOutput("output4", turnoutBox4.getDisplayName()); // store choice from turnoutBox4
299                        turnoutBox4.updateComment(turnoutBox4.getNamedBean(), newMastName + ":output4");
300                        if (bitNum > 4) {
301                            currentMast.setOutput("output5", turnoutBox5.getDisplayName()); // store choice from turnoutBox5
302                            turnoutBox5.updateComment(turnoutBox5.getNamedBean(), newMastName + ":output5");
303                            if (bitNum > 5) {
304                                currentMast.setOutput("output6", turnoutBox6.getDisplayName()); // store choice from turnoutBox6
305                                turnoutBox6.updateComment(turnoutBox6.getNamedBean(), newMastName + ":output6");
306                                if (bitNum > 6) {
307                                    currentMast.setOutput("output7", turnoutBox7.getDisplayName()); // store choice from turnoutBox7
308                                    turnoutBox7.updateComment(turnoutBox7.getNamedBean(), newMastName + ":output7");
309                                    if (bitNum > 7) {
310                                        currentMast.setOutput("output8", turnoutBox8.getDisplayName()); // store choice from turnoutBox8
311                                        turnoutBox8.updateComment(turnoutBox8.getNamedBean(), newMastName + ":output8");
312                                        if (bitNum > 8) {
313                                            currentMast.setOutput("output9", turnoutBox9.getDisplayName()); // store choice from turnoutBox9
314                                            turnoutBox9.updateComment(turnoutBox9.getNamedBean(), newMastName + ":output9");
315                                            if (bitNum > 9) {
316                                                currentMast.setOutput("output10", turnoutBox10.getDisplayName()); // store choice from turnoutBox10
317                                                turnoutBox10.updateComment(turnoutBox10.getNamedBean(), newMastName + ":output10");
318                                                // repeat in order to extend MAXMATRIXBITS
319                                            }
320                                        }
321                                    }
322                                }
323                            }
324                        }
325                    }
326                }
327            }
328        } catch (JmriException e) {
329            log.warn("bean not found");
330        }
331
332        for (Map.Entry<String, MatrixAspectPanel> entry : matrixAspect.entrySet()) {
333            // store matrix in mast per aspect, compare with line 991
334            matrixMastPanel.add(entry.getValue().getPanel()); // read from aspect panel to mast
335            if (matrixAspect.get(entry.getKey()).isAspectDisabled()) {
336                currentMast.setAspectDisabled(entry.getKey()); // don't store bits when this aspect is disabled
337            } else {
338                currentMast.setAspectEnabled(entry.getKey());
339                currentMast.setBitsForAspect(entry.getKey(), matrixAspect.get(entry.getKey()).trimAspectBits()); // return as char[]
340            }
341        }
342        currentMast.resetPreviousStates(resetPreviousState.isSelected()); // read from panel, not displayed?
343
344        currentMast.setAllowUnLit(allowUnLit.isSelected());
345
346        // copy bits from UnLitPanel var unLitPanelBits
347        try {
348            currentMast.setUnLitBits(trimUnLitBits());
349        } catch (Exception ex) {
350            log.error("failed to read and copy unLitPanelBits");
351        }
352        
353        if (!username.isEmpty()) {
354            currentMast.setUserName(username);
355        }
356
357        prefs.setComboBoxLastSelection(matrixBitNumSelectionCombo, (String) columnChoice.getSelectedItem()); // store bitNum pref
358        
359        currentMast.setAllowUnLit(allowUnLit.isSelected());
360
361        // set MatrixMast specific Delay information, see jmri.implementation.MatrixSignalMast
362        int addDelay = (Integer) timeDelay.getValue(); // from a JSpinner with 0 set as minimum
363        currentMast.setMatrixMastCommandDelay(addDelay);
364        log.debug("mast create completed successfully");
365        return true;
366    }
367
368    /**
369     * Create bitNumPanel with drop down to set number of columns, separate from
370     * the rest for redraw.
371     * <p>
372     * Auto refresh to show/hide input (turnout) selection boxes. Hide/show
373     * checkboxes in matrix (per aspect).
374     *
375     * @return a JPanel with a comboBox to select number of outputs, set at
376     *         current value
377     */
378    private JPanel makeMatrixMastBitnumPanel() {
379        JPanel bitnumpanel = new JPanel();
380        bitnumpanel.setLayout(new FlowLayout(FlowLayout.LEADING));
381        // select number of columns in logic matrix
382        bitnumpanel.add(bitNumLabel);
383        bitnumpanel.add(columnChoice);
384        if (bitNum < 1 || bitNum > MAXMATRIXBITS) {
385            bitNum = 4; // default to 4 col if out-of-range for some reason
386        }
387        columnChoice.setSelectedIndex(bitNum - 1);
388        columnChoice.addActionListener((ActionEvent e) -> {
389            String newBitnumString = (String) columnChoice.getSelectedItem();
390            if (newBitnumString == null) {
391                newBitnumString = "4"; // error, fall back to default
392                log.debug("null newBitnumString in makeMatrixMastBitnumPanel()");
393            }
394            bitNumChanged(Integer.valueOf(newBitnumString));
395        });
396        return bitnumpanel;
397    }
398
399    /**
400     * @return char[] of length bitNum copied from unLitPanelBits
401     */
402    private char[] trimUnLitBits() {
403        if (unLitPanelBits != null) {
404            return Arrays.copyOf(unLitPanelBits, bitNum);
405        } else {
406            return Arrays.copyOf(emptyBits, bitNum);
407        }
408    }
409
410    /**
411     * Build lower half of Add Signal Mast panel, specifically for Matrix Mast.
412     * <p>
413     * Called when Mast Type drop down changes.
414     */
415    private void updateMatrixMastPanel() {
416        matrixAspect = new LinkedHashMap<>(NOTIONAL_ASPECT_COUNT);
417
418        // nothing to do if no map yet present
419        if (map == null) return;
420        
421        Enumeration<String> aspects = map.getAspects();
422        while (aspects.hasMoreElements()) {
423            String aspect = aspects.nextElement();
424            MatrixAspectPanel aspectpanel = new MatrixAspectPanel(aspect);
425            matrixAspect.put(aspect, aspectpanel); // store in LinkedHashMap
426            // values are filled in later
427        }
428        matrixMastPanel.removeAll();
429
430        // sub panels (so we can hide all turnouts with Output Type drop down box later)
431        JPanel turnoutpanel = new JPanel();
432        
433        // binary matrix outputs follow:
434        
435        JPanel output1panel = makeOutputPanel(turnoutBox1,1);
436        turnoutpanel.add(output1panel);
437        // output1panel always on
438        output1panel.setVisible(true);
439        
440        turnoutpanel.add(makeOutputPanel(turnoutBox2,2));
441        turnoutpanel.add(makeOutputPanel(turnoutBox3,3));
442        turnoutpanel.add(makeOutputPanel(turnoutBox4,4));
443        
444        // Four in a row is really enough, start another row
445        matrixMastPanel.add(turnoutpanel);
446        turnoutpanel = new JPanel();
447        
448        turnoutpanel.add(makeOutputPanel(turnoutBox5,5));
449        turnoutpanel.add(makeOutputPanel(turnoutBox6,6));
450        turnoutpanel.add(makeOutputPanel(turnoutBox7,7));
451        turnoutpanel.add(makeOutputPanel(turnoutBox8,8));
452
453        // Four in a row is really enough, start another row
454        matrixMastPanel.add(turnoutpanel);
455        turnoutpanel = new JPanel();
456
457        turnoutpanel.add(makeOutputPanel(turnoutBox9,9));
458        turnoutpanel.add(makeOutputPanel(turnoutBox10,10));
459
460        // repeat in order to extend MAXMATRIXBITS
461
462        matrixMastPanel.add(turnoutpanel);
463
464        // show the active unlit check boxes
465        for (int i = 0; i < MAXMATRIXBITS; i++) {
466            unlitCheckArray[i].setVisible(i < bitNum);
467        }
468
469        JPanel matrixHeader = new JPanel();
470        JLabel matrixHeaderLabel = new JLabel(Bundle.getMessage("AspectMatrixHeaderLabel", bitNum), JLabel.CENTER);
471        matrixHeader.add(matrixHeaderLabel);
472        matrixHeaderLabel.setToolTipText(Bundle.getMessage("AspectMatrixHeaderTooltip"));
473        matrixMastPanel.add(matrixHeader);
474
475        for (Map.Entry<String, MatrixAspectPanel> entry : matrixAspect.entrySet()) {
476            matrixMastPanel.add(entry.getValue().getPanel()); // load Aspect sub panels to matrixMastPanel from hashmap
477            // build aspect sub panels
478        }
479        if ((matrixAspect.size() & 1) == 1) {
480            // spacer before "Reset previous aspect"
481            matrixMastPanel.add(new JLabel());
482        }
483
484        matrixMastPanel.add(resetPreviousState); // checkbox
485        
486        resetPreviousState.setToolTipText(Bundle.getMessage("ResetPreviousToolTip"));
487        // copy option matrixMast bitstrings = settings
488        JPanel matrixCopyPanel = new JPanel();
489        matrixCopyPanel.setLayout(new FlowLayout(FlowLayout.LEADING));
490        matrixCopyPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("MatrixMastCopyAspectBits"))));
491        matrixCopyPanel.add(copyFromMastSelection());
492        matrixMastPanel.add(matrixCopyPanel);
493
494        // add additional MatrixMast-specific delay
495        JPanel delayPanel = new JPanel();
496        delayPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("LabelTurnoutDelay"))));
497        timeDelay.setModel(new SpinnerNumberModel(0, 0, 1000, 1));
498        // timeDelay.setValue(0); // reset from possible previous use
499        timeDelay.setPreferredSize(new JTextField(5).getPreferredSize());
500        delayPanel.add(timeDelay);
501        timeDelay.setToolTipText(Bundle.getMessage("TooltipTurnoutDelay"));
502        delayPanel.add(new JLabel(Bundle.getMessage("LabelMilliseconds")));
503        matrixMastPanel.add(delayPanel);
504
505        matrixMastPanel.setLayout(new jmri.util.javaworld.GridLayout2(0, 1)); // 0 means enough
506        matrixMastPanel.revalidate();
507    }
508    
509    /**
510     * Index is 1-based here:  1..MAXMATRIXBITS
511     * @param turnoutPanel The panel to convert to an output panel
512     * @param index The 1..MAXMATRIXBITS index of the one to be set visible
513     * @return An output panel containing the selected turnout panel
514     */
515    JPanel makeOutputPanel(JPanel turnoutPanel, int index) {
516        JPanel outputpanel = new JPanel();
517        outputpanel.add(turnoutPanel);
518        TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
519        border.setTitle(Bundle.getMessage("MatrixOutputLabel", index));
520        outputpanel.setBorder(border);
521        
522        outputpanel.setVisible(index <= bitNum);
523        
524        return outputpanel;
525    }
526    
527    /**
528     * When the user changes the number of columns in matrix from the drop down:
529     * store the new value and redraw pane.
530     *
531     * @param newColNum int with the new value = the number of columns in the
532     *                  Matrix Table
533     */
534     private void bitNumChanged(Integer newColNum) {
535        if (newColNum < 1 || newColNum > MAXMATRIXBITS || newColNum == bitNum) {
536            return;
537        }
538        bitNum = newColNum;
539        // hide/show output choices per Aspect
540        updateMatrixMastPanel();
541
542        validate();
543        java.awt.Container ancestor = getTopLevelAncestor();
544        if ((ancestor instanceof JmriJFrame)) {
545            ancestor.setSize(ancestor.getPreferredSize());
546            ((JmriJFrame) ancestor).pack();
547        }
548        repaint();
549    }
550
551    private void copyFromAnotherMatrixMastAspect(String strMast) {
552        MatrixSignalMast mast = (MatrixSignalMast) InstanceManager.getDefault(SignalMastManager.class).getNamedBean(strMast);
553        if (mast == null) {
554            log.error("Cannot copy from mast {} which doesn't exist", strMast);
555            return;
556        }
557        if (bitNum != mast.getBitNum()) {
558            int i = JOptionPane.showConfirmDialog(null, Bundle.getMessage("MatrixColWarning", mast.getBitNum(), bitNum),
559                    Bundle.getMessage("MatrixColWarningTitle"),
560                    JOptionPane.YES_NO_OPTION);
561            if (i != 0) {
562                return;
563            }
564        }
565        // cf. line 405 loading an existing mast for edit
566        for (Map.Entry<String, MatrixAspectPanel> entry : matrixAspect.entrySet()) {
567            // select the correct checkboxes
568            MatrixAspectPanel matrixPanel = entry.getValue(); // load aspectpanel from hashmap
569            matrixPanel.setAspectDisabled(mast.isAspectDisabled(entry.getKey())); // sets a disabled aspect
570            if (!mast.isAspectDisabled(entry.getKey())) {
571                char[] mastBits = mast.getBitsForAspect(entry.getKey()); // same as loading an existing MatrixMast
572                char[] panelAspectBits = Arrays.copyOf(mastBits, MAXMATRIXBITS); // store as 6 character array in panel
573                matrixPanel.updateAspectBits(panelAspectBits);
574                matrixPanel.setAspectBoxes(panelAspectBits);
575                // sets boxes 1 - MAXMATRIXBITS on aspect sub panel from values in hashmap char[] like: 1001
576            }
577        }
578    }
579
580/*    *//**
581     * Call for sub panel per aspect from hashmap matrixAspect with check boxes
582     * to set properties.
583     * <p>
584     * Invoked when updating MatrixMastPanel.
585     *
586     * @see #updateMatrixMastPanel()
587     *//*
588    void updateMatrixAspectPanel() {
589        Enumeration<String> aspects = map.getAspects();
590        while (aspects.hasMoreElements()) {
591            String aspect = aspects.nextElement();
592            MatrixAspectPanel aspectpanel = new MatrixAspectPanel(aspect, bitString); // build 1 line, picking up bitString
593            matrixAspect.put(aspect, aspectpanel); // store that line
594        }
595        // refresh aspects list
596        // TODO sort matrixAspect HashTable, which at this point is not sorted
597        matrixMastPanel.removeAll();
598        for (Map.Entry<String, MatrixAspectPanel> entry : matrixAspect.entrySet()) {
599            matrixMastPanel.add(entry.getValue().getPanel());
600            // Matrix checkbox states are set by getPanel()
601        }
602        matrixMastPanel.setLayout(new jmri.util.javaworld.GridLayout2(0, 1)); // 0 means enough
603        matrixMastPanel.revalidate();
604    }*/
605
606    private JPanel matrixUnLitPanel = new JPanel();
607    
608    private JCheckBox[] unlitCheckArray = new JCheckBox[MAXMATRIXBITS];
609    {
610        for (int i = 0; i < MAXMATRIXBITS; i++) {
611            unlitCheckArray[i] = new JCheckBox();
612        }
613    }
614    
615    // add more JCheckBoxes in order to extend MAXMATRIXBITS
616
617    /**
618     * JPanel to set outputs for an unlit (Dark) Matrix Signal Mast.
619     */
620    private void matrixUnLitPanel() {
621        if (bitNum < 1 || bitNum > MAXMATRIXBITS) {
622            bitNum = 4; // default to 4 col for (first) new mast
623        }
624
625        JPanel matrixUnLitDetails = new JPanel();
626        matrixUnLitDetails.setLayout(new jmri.util.javaworld.GridLayout2(1, 1)); // stretch to full width
627
628        for (int i = 0; i < MAXMATRIXBITS; i++) matrixUnLitDetails.add(unlitCheckArray[i]);
629
630        matrixUnLitPanel.add(matrixUnLitDetails);
631        TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
632        border.setTitle(Bundle.getMessage("MatrixUnLitDetails"));
633        matrixUnLitPanel.setBorder(border);
634        matrixUnLitPanel.setToolTipText(Bundle.getMessage("MatrixUnlitTooltip"));
635
636        unlitCheckArray[0].addActionListener((ActionEvent e) -> {
637            setUnLitBit(1, unlitCheckArray[0].isSelected());
638        });
639        unlitCheckArray[1].addActionListener((ActionEvent e) -> {
640            setUnLitBit(2, unlitCheckArray[1].isSelected());
641        });
642        unlitCheckArray[2].addActionListener((ActionEvent e) -> {
643            setUnLitBit(3, unlitCheckArray[2].isSelected());
644        });
645        unlitCheckArray[3].addActionListener((ActionEvent e) -> {
646            setUnLitBit(4, unlitCheckArray[3].isSelected());
647        });
648        unlitCheckArray[4].addActionListener((ActionEvent e) -> {
649            setUnLitBit(5, unlitCheckArray[4].isSelected());
650        });
651        unlitCheckArray[5].addActionListener((ActionEvent e) -> {
652            setUnLitBit(6, unlitCheckArray[5].isSelected());
653        });
654        unlitCheckArray[6].addActionListener((ActionEvent e) -> {
655            setUnLitBit(7, unlitCheckArray[6].isSelected());
656        });
657        unlitCheckArray[7].addActionListener((ActionEvent e) -> {
658            setUnLitBit(8, unlitCheckArray[7].isSelected());
659        });
660        unlitCheckArray[8].addActionListener((ActionEvent e) -> {
661            setUnLitBit(9, unlitCheckArray[8].isSelected());
662        });
663        unlitCheckArray[9].addActionListener((ActionEvent e) -> {
664            setUnLitBit(10, unlitCheckArray[9].isSelected());
665        });
666        // repeat in order to extend MAXMATRIXBITS
667    }
668
669
670    private JComboBox<String> copyFromMastSelection() {
671        JComboBox<String> mastSelect = new JComboBox<>();
672        for (SignalMast mast : InstanceManager.getDefault(SignalMastManager.class).getNamedBeanSet()) {
673            if (mast instanceof MatrixSignalMast){
674                mastSelect.addItem(mast.getDisplayName());
675            }
676        }
677        if (mastSelect.getItemCount() == 0) {
678            mastSelect.setEnabled(false);
679        } else {
680            mastSelect.insertItemAt("", 0);
681            mastSelect.setSelectedIndex(0);
682            mastSelect.addActionListener((ActionEvent e) -> {
683                @SuppressWarnings("unchecked") // e.getSource() cast from mastSelect source
684                JComboBox<String> eb = (JComboBox<String>) e.getSource();
685                String sourceMast = (String) eb.getSelectedItem();
686                if (sourceMast != null && !sourceMast.isEmpty()) {
687                    copyFromAnotherMatrixMastAspect(sourceMast);
688                }
689            });
690        }
691        return mastSelect;
692    }
693
694    /**
695     * Update the on/off positions for the unLitPanelBits char[].
696     * <p>
697     * Invoked from bit checkboxes 1 to MAXMATRIXBITS on unLitPanel.
698     *
699     * @param column int as index for an output (between 1 and 6)
700     * @param state  boolean for the output On (Closed) or Off (Thrown)
701     */
702    private void setUnLitBit(int column, boolean state) {
703        if (state) {
704            unLitPanelBits[column - 1] = '1';
705        } else {
706            unLitPanelBits[column - 1] = '0';
707        }
708    }
709
710    /**
711     * Check all aspects for duplicate bit combos.
712     *
713     * @return true if at least 1 duplicate row of bits is found
714     */
715    private boolean identicalBits() {
716        boolean identical = false;
717        numberOfActiveAspects = 0;
718        Collection<String> seenBits = new HashSet<>(); // a fast access, no duplicates Collection of bit combinations
719        for (Map.Entry<String, MatrixAspectPanel> entry : matrixAspect.entrySet()) {
720            // check per aspect
721            if (entry.getValue().isAspectDisabled()) {
722                continue; // skip disabled aspects
723            } else if (seenBits.contains(String.valueOf(entry.getValue().trimAspectBits()))) {
724                identical = true;
725                log.debug("-found duplicate {}", String.valueOf(entry.getValue().trimAspectBits()));
726                // don't break, so we can count number of enabled aspects for this mast
727            } else {
728                seenBits.add(String.valueOf(entry.getValue().trimAspectBits())); // convert back from char[] to String
729                log.debug("-added new {}; seenBits = {}", String.valueOf(entry.getValue().trimAspectBits()), seenBits.toString());
730            }
731            ++numberOfActiveAspects;
732        }
733        return identical;
734    }
735
736    /**
737     * JPanel to define properties of an Aspect for a Matrix Signal Mast.
738     * <p>
739     * Invoked from the AddSignalMastPanel class when Output Matrix Signal Mast is
740     * selected as mast type.
741     *
742     * @author Egbert Broerse
743     */
744    class MatrixAspectPanel {
745
746        JCheckBox disabledCheck = new JCheckBox(Bundle.getMessage("DisableAspect"));
747        JCheckBox bitCheck1 = new JCheckBox();
748        JCheckBox bitCheck2 = new JCheckBox();
749        JCheckBox bitCheck3 = new JCheckBox();
750        JCheckBox bitCheck4 = new JCheckBox();
751        JCheckBox bitCheck5 = new JCheckBox();
752        JCheckBox bitCheck6 = new JCheckBox();
753        JCheckBox bitCheck7 = new JCheckBox();
754        JCheckBox bitCheck8 = new JCheckBox();
755        JCheckBox bitCheck9 = new JCheckBox();
756        JCheckBox bitCheck10= new JCheckBox();
757        // repeat in order to extend MAXMATRIXBITS
758        
759        JTextField aspectBitsField = new JTextField(MAXMATRIXBITS); // for debug
760        String aspect = "";
761        String emptyChars = "0000000000"; // size of String = MAXMATRIXBITS; add another 0 in order to extend MAXMATRIXBITS
762        char[] aspectBits = emptyChars.toCharArray();
763
764        /**
765         * Build new aspect matrix panel.
766         * Called when Add Signal Mast Pane is built.
767         *
768         * @param aspect String like "Clear"
769         */
770        MatrixAspectPanel(String aspect) {
771            this.aspect = aspect;
772        }
773
774        /**
775         * Build an aspect matrix panel using char[] previously entered. Called
776         * when number of outputs = columns is changed (possible during new mast
777         * creation only).
778         *
779         * @param aspect    String like "Clear"
780         * @param panelBits char[] of up to 5 1's and 0's
781         */
782        MatrixAspectPanel(String aspect, char[] panelBits) {
783            if (panelBits == null || panelBits.length == 0) {
784                return;
785            }
786            this.aspect = aspect;
787            // aspectBits is char[] of length(bitNum) describing state of on/off checkboxes
788            // i.e. "0101" provided as char[] array
789            // easy to manipulate by index
790            // copy to checkbox states:
791            aspectBits = panelBits;
792            setAspectBoxes(aspectBits);
793        }
794
795        void updateAspectBits(char[] newBits) {
796            aspectBits = newBits;
797        }
798
799        boolean isAspectDisabled() {
800            return disabledCheck.isSelected();
801        }
802
803        /**
804         * Set an Aspect Panels elements inactive.
805         * <p>
806         * Invoked from Disabled (aspect) checkbox and from Edit mast pane.
807         *
808         * @param boo true (On) or false (Off)
809         */
810        void setAspectDisabled(boolean boo) {
811            disabledCheck.setSelected(boo);
812            if (boo) { // disable all (output bit) checkboxes on this aspect panel
813                // aspectBitsField always Disabled
814                bitCheck1.setEnabled(false);
815                if (bitNum > 1) {
816                    bitCheck2.setEnabled(false);
817                }
818                if (bitNum > 2) {
819                    bitCheck3.setEnabled(false);
820                }
821                if (bitNum > 3) {
822                    bitCheck4.setEnabled(false);
823                }
824                if (bitNum > 4) {
825                    bitCheck5.setEnabled(false);
826                }
827                if (bitNum > 5) {
828                    bitCheck6.setEnabled(false);
829                }
830                if (bitNum > 6) {
831                    bitCheck7.setEnabled(false);
832                }
833                if (bitNum > 7) {
834                    bitCheck8.setEnabled(false);
835                }
836                if (bitNum > 8) {
837                    bitCheck9.setEnabled(false);
838                }
839                if (bitNum > 9) {
840                    bitCheck10.setEnabled(false);
841                }
842                // repeat in order to extend MAXMATRIXBITS
843                
844            } else { // enable all (output bit) checkboxes on this aspect panel
845                // aspectBitsField always Disabled
846                bitCheck1.setEnabled(true);
847                if (bitNum > 1) {
848                    bitCheck2.setEnabled(true);
849                }
850                if (bitNum > 2) {
851                    bitCheck3.setEnabled(true);
852                }
853                if (bitNum > 3) {
854                    bitCheck4.setEnabled(true);
855                }
856                if (bitNum > 4) {
857                    bitCheck5.setEnabled(true);
858                }
859                if (bitNum > 5) {
860                    bitCheck6.setEnabled(true);
861                }
862                if (bitNum > 6) {
863                    bitCheck7.setEnabled(true);
864                }
865                if (bitNum > 7) {
866                    bitCheck8.setEnabled(true);
867                }
868                if (bitNum > 8) {
869                    bitCheck9.setEnabled(true);
870                }
871                if (bitNum > 9) {
872                    bitCheck10.setEnabled(true);
873                }
874                // repeat in order to set extend MAXMATRIXBITS
875            }
876        }
877
878        /**
879         * Update the on/off positions for an Aspect in the aspectBits char[].
880         * <p>
881         * Invoked from bit checkboxes 1 to MAXMATRIXBITS on aspectPanels.
882         *
883         * @param column int of the output (between 1 and MAXMATRIXBITS)
884         * @param state  boolean for the output On (Closed) or Off (Thrown)
885         * @see #aspectBits
886         */
887        void setBit(int column, boolean state) {
888            if (state) {
889                aspectBits[column - 1] = '1';
890            } else {
891                aspectBits[column - 1] = '0';
892            }
893            String value = String.valueOf(aspectBits); // convert back from char[] to String
894            aspectBitsField.setText(value);
895        }
896
897        /**
898         * Send the on/off positions for an Aspect to mast.
899         *
900         * @return A char[] of '1' and '0' elements with a length between 1 and
901         *         5 corresponding with the number of outputs for this mast
902         * @see jmri.implementation.MatrixSignalMast
903         */
904        char[] trimAspectBits() {
905            try {
906                return Arrays.copyOf(aspectBits, bitNum); // copy to new char[] of length bitNum
907            } catch (Exception ex) {
908                log.error("failed to read and copy aspectBits");
909                return new char[] {};
910            }
911        }
912
913        /**
914         * Activate the corresponding checkboxes on a MatrixApectPanel.
915         *
916         * @param aspectBits A char[] of '1' and '0' elements with a length
917         *                   between 1 and 5 corresponding with the number of
918         *                   outputs for this mast
919         */
920        private void setAspectBoxes(char[] aspectBits) {
921            bitCheck1.setSelected(aspectBits[0] == '1');
922            if (bitNum > 1) {
923                bitCheck2.setSelected(aspectBits[1] == '1');
924            }
925            if (bitNum > 2) {
926                bitCheck3.setSelected(aspectBits[2] == '1');
927            }
928            if (bitNum > 3) {
929                bitCheck4.setSelected(aspectBits[3] == '1');
930            }
931            if (bitNum > 4) {
932                bitCheck5.setSelected(aspectBits[4] == '1');
933            }
934            if (bitNum > 5) {
935                bitCheck6.setSelected(aspectBits[5] == '1');
936            }
937            if (bitNum > 6) {
938                bitCheck7.setSelected(aspectBits[6] == '1');
939            }
940            if (bitNum > 7) {
941                bitCheck8.setSelected(aspectBits[7] == '1');
942            }
943            if (bitNum > 8) {
944                bitCheck9.setSelected(aspectBits[8] == '1');
945            }
946            if (bitNum > 9) {
947                bitCheck10.setSelected(aspectBits[9] == '1');
948            }
949            // repeat in order to set extend MAXMATRIXBITS
950            
951            String value = String.valueOf(aspectBits); // convert back from char[] to String
952            aspectBitsField.setText(value);
953        }
954
955        JPanel panel;
956
957        /**
958         * Build a JPanel for an Aspect Matrix row.
959         *
960         * @return JPanel to be displayed on the Add/Edit Signal Mast panel
961         */
962        JPanel getPanel() {
963            if (panel == null) {
964                panel = new JPanel();
965                panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
966
967                JPanel matrixDetails = new JPanel();
968                matrixDetails.add(disabledCheck);
969                matrixDetails.add(bitCheck1);
970                matrixDetails.add(bitCheck2);
971                matrixDetails.add(bitCheck3);
972                matrixDetails.add(bitCheck4);
973                matrixDetails.add(bitCheck5);
974                matrixDetails.add(bitCheck6);
975                matrixDetails.add(bitCheck7);
976                matrixDetails.add(bitCheck8);
977                matrixDetails.add(bitCheck9);
978                matrixDetails.add(bitCheck10);
979                // repeat in order to extend MAXMATRIXBITS
980                
981                // TODO refresh aspectSetting, can be in OKPressed() to store/warn for duplicates (with button 'Ignore')
982                
983                // hide if id > bitNum (var in panel)
984                bitCheck2.setVisible(bitNum > 1);
985                bitCheck3.setVisible(bitNum > 2);
986                bitCheck4.setVisible(bitNum > 3);
987                bitCheck5.setVisible(bitNum > 4);
988                bitCheck6.setVisible(bitNum > 5);
989                bitCheck7.setVisible(bitNum > 6);
990                bitCheck8.setVisible(bitNum > 7);
991                bitCheck9.setVisible(bitNum > 8);
992                bitCheck10.setVisible(bitNum> 9);
993                // repeat in order to extend MAXMATRIXBITS
994
995                matrixDetails.add(aspectBitsField);
996                aspectBitsField.setEnabled(false); // not editable, just for debugging
997                aspectBitsField.setVisible(false); // set to true to check/debug
998
999                panel.add(matrixDetails);
1000                TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
1001                border.setTitle(aspect);
1002                panel.setBorder(border);
1003
1004                disabledCheck.addActionListener((ActionEvent e) -> {
1005                    setAspectDisabled(disabledCheck.isSelected());
1006                });
1007
1008                bitCheck1.addActionListener((ActionEvent e) -> {
1009                    setBit(1, bitCheck1.isSelected());
1010                });
1011                bitCheck2.addActionListener((ActionEvent e) -> {
1012                    setBit(2, bitCheck2.isSelected());
1013                });
1014                bitCheck3.addActionListener((ActionEvent e) -> {
1015                    setBit(3, bitCheck3.isSelected());
1016                });
1017                bitCheck4.addActionListener((ActionEvent e) -> {
1018                    setBit(4, bitCheck4.isSelected());
1019                });
1020                bitCheck5.addActionListener((ActionEvent e) -> {
1021                    setBit(5, bitCheck5.isSelected());
1022                });
1023                bitCheck6.addActionListener((ActionEvent e) -> {
1024                    setBit(6, bitCheck6.isSelected());
1025                });
1026                bitCheck7.addActionListener((ActionEvent e) -> {
1027                    setBit(7, bitCheck7.isSelected());
1028                });
1029                bitCheck8.addActionListener((ActionEvent e) -> {
1030                    setBit(8, bitCheck8.isSelected());
1031                });
1032                bitCheck9.addActionListener((ActionEvent e) -> {
1033                    setBit(9, bitCheck9.isSelected());
1034                });
1035                bitCheck10.addActionListener((ActionEvent e) -> {
1036                    setBit(10, bitCheck10.isSelected());
1037                });
1038                // repeat in order to extend MAXMATRIXBITS
1039            }
1040            return panel;
1041        }
1042
1043    }
1044
1045    @ServiceProvider(service = SignalMastAddPane.SignalMastAddPaneProvider.class)
1046    static public class SignalMastAddPaneProvider extends SignalMastAddPane.SignalMastAddPaneProvider {
1047        /** {@inheritDoc} */
1048        @Override
1049        @Nonnull public String getPaneName() {
1050            return Bundle.getMessage("MatrixCtlMast");
1051        }
1052        /** {@inheritDoc} */
1053        @Override
1054        @Nonnull public SignalMastAddPane getNewPane() {
1055            return new MatrixSignalMastAddPane();
1056        }
1057    }
1058
1059    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MatrixSignalMastAddPane.class);
1060
1061}