001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Dimension;
005import java.awt.GridBagConstraints;
006import java.awt.GridBagLayout;
007import java.awt.Insets;
008import java.util.*;
009import java.util.stream.IntStream;
010import javax.swing.ButtonGroup;
011import javax.swing.ImageIcon;
012import javax.swing.JButton;
013import javax.swing.JComponent;
014import javax.swing.JLabel;
015import javax.swing.JPanel;
016import javax.swing.JRadioButton;
017import javax.swing.JScrollPane;
018import javax.swing.JTextField;
019
020import jmri.jmrit.roster.RosterEntry;
021import jmri.util.CvUtil;
022import jmri.util.FileUtil;
023import jmri.util.jdom.LocaleSelector;
024import jmri.util.swing.JmriJOptionPane;
025
026import org.jdom2.*;
027
028/**
029 * Provide a graphical representation of the ESU mapping table. Each row
030 * represents a possible mapping between input conditions (function keys, etc.)
031 * and logical, physical or sound outputs.
032 * <p>
033 * Uses data from the "model" and "family" elements from the decoder definition
034 * file to configure the number of rows and set up any custom item names:
035 * <dl>
036 * <dt>extFnsESU</dt>
037 * <dd>Uses the ESU-style function map rather than the NMRA style.</dd>
038 * <dd>&nbsp;&nbsp;&nbsp;extFnsESU="V4" for generation 4 decoders.</dd>
039 * <dd>&nbsp;&nbsp;&nbsp;extFnsESU="V5" for generation 5 decoders.</dd>
040 * <dd>&nbsp;</dd>
041 * <dt>numOuts</dt>
042 * <dd>Number of physical outputs (information only, not used by the code).</dd>
043 * <dd>&nbsp;</dd>
044 * <dt>numOutsFromDefinition</dt>
045 * <dd>Number of physical outputs read from decoder definition.</dd>
046 * <dd>&nbsp;</dd>
047 * <dt>numFns</dt>
048 * <dd>Number of mapping rows to display.</dd>
049 * <dd><em>Only use this parameter if the specific decoder definition implements
050 * less rows than the default for that decoder generation (V4/V5), for example
051 * the LokPilot V4.</em></dd>
052 * <dd>&nbsp;</dd>
053 * <dt>output (in "family" or "model")</dt>
054 * <dd>name="blockNo,itemNo" label="theName"</dd>
055 * <dd>&nbsp;-&nbsp;Set name of block "blockNo", item "itemNo" to
056 * "theName".</dd>
057 * <dd>&nbsp;</dd>
058 * <dd>name="blockNo,itemNo" label="theName|OnChoice|OffChoice"</dd>
059 * <dd>&nbsp;-&nbsp;Set name of block "blockNo", item "itemNo" to "theName" and
060 * replace the default "On and "Off" choices for enumChoice items.</dd>
061 * <dd>&nbsp;</dd>
062 * <dd>name="blockNo,itemNo" label="|"</dd>
063 * <dd>&nbsp;-&nbsp;Cause item block "blockNo", item "itemNo" to be suppressed
064 * from the table.</dd>
065 * <dd>&nbsp;</dd>
066 * <dd>name="itemNo" label="..."</dd>
067 * <dd>&nbsp;-&nbsp;As above, but using an absolute "itemNo" (not
068 * recommended).</dd>
069 * <dd>&nbsp;</dd>
070 * <dd>name="theName" label="OnChoice|OffChoice"</dd>
071 * <dd>&nbsp;-&nbsp;Set name of the nth item to "theName" and replace the
072 * default "On and "Off" choices for enumChoice items, where this line is the
073 * nth "output" element of the "model" element in the decoder definition file
074 * (not recommended).</dd>
075 * </dl>
076 * <dl>
077 * <dt>Default item headings:</dt>
078 * <dd>Coded in String array itemDescESU[] of this class.</dd>
079 * <dd>Item headings can be overridden by the "output" elements documented
080 * above.</dd>
081 * </dl>
082 * <dl>
083 * <dt>Items will be suppressed if any of the following are true:</dt>
084 * <dd>No variables are found for that item.</dd>
085 * <dd>The item output name is of the form name="n" label="|".</dd>
086 * <dd>Item number is &gt; numOuts.</dd>
087 * </dl>
088 * <dl>
089 * <dt>Variable definitions:</dt>
090 * <dd>Are of the form "ESU Function Row xx Item yy" and are created "on the
091 * fly" by this class. Many thousands of variables are needed to populate the
092 * function map. It is more efficient to create these in code than to use XML in
093 * the decoder file. <strong>DO NOT</strong> specify them in the decoder
094 * file.</dd>
095 * <dd><br>
096 * The "tooltip" &amp; "label" attributes on a fnmapping variable are ignored.
097 * Expanded internationalized tooltips are generated in the code.
098 * </dd>
099 * </dl>
100 *
101 * @author Bob Jacobsen Copyright (C) 2001
102 * @author Dave Heap Copyright (C) 2016, 2019
103 */
104public final class FnMapPanelESU extends JPanel {
105
106    // columns
107    int firstCol = 0;
108    int firstOut = 2;
109
110    int currentCol = firstCol;
111
112    // rows
113    static final int HINTS_ROW = 0;
114    static final int MOVE_ARROWS_TOP_ROW = 1;
115    static final int BLOCK_NAME_ROW = 1;
116    static final int FIRST_ROW = BLOCK_NAME_ROW + 2;
117    static final int ROW_LABEL_ROW = FIRST_ROW - 1;
118
119    int currentRow = FIRST_ROW;
120
121    static final int PI_CV = 16;
122    static final int SI_START_CV = 2;
123    static final int SI_CV_MODULUS = 16;
124    static final int START_CV = 257;
125    static final int CV_PAGE_MODULUS = 256;
126    static final int BIT_MODULUS = 8;
127
128    GridBagLayout gl = null;
129    GridBagConstraints cs = null;
130    VariableTableModel varModel;
131    // Titles for blocks of items.
132    String[] outBlockName;
133    // Number of items per block.
134    int[] outBlockLength;
135    int[] outBlockSiStartCv;
136    int[] outBlockSiCvModulus;
137    int[] outBlockStartCv;
138    int[] outBlockCvModulus;
139    // Number of bits per block item.
140    int[] outBlockItemBits;
141    // Starting column column of block.
142    int[] outBlockStartCol;
143    // Number of used items per block.
144    int[] outBlockUsed;
145    JTextField[][] summaryLine;
146    int maxItems;
147    // Default item labels.
148    String[] itemDescESU;
149    /**
150     * Default item labels.
151     * <dl>
152     * <dt>Two rows are available for item labels</dt>
153     * <dd>Use the '|' character to designate a row break</dd>
154     * </dl>
155     * <p>
156     * Item labels can be overridden by the "output" element of the "model" or
157     * "family" element from the decoder definition file.
158     */
159    String[] itemLabel;
160    String[][] itemName;
161    boolean[] itemIsUsed;
162    int[][] iVarIndex;
163
164    // default values
165    String extFnsESU = "no";
166    int numItems = -1;
167    int numFns = 1;
168    int numRows = 5;
169    int numOuts = 2;
170    // numOuts above is used for calculating correct offsets
171    // the following is actually read from the definition
172    int numOutsFromDefinition = numOuts;
173    int numStates = 2;
174    int numWheelSensors = 1;
175    int numReserved = 1;
176    int numSensors = 4;
177    int numConfig2 = 4;
178    int numLogic = 1;
179    int numSounds = 1;
180
181    // for row moves
182    int selectedRow = -1;
183    JRadioButton[] rowButton;
184
185    public FnMapPanelESU(VariableTableModel v, List<Integer> varsUsed,
186            Element model, RosterEntry rosterEntry, CvTableModel cvModel) {
187
188        // retrieve function map version
189        Attribute a = model.getAttribute("extFnsESU");
190        try {
191            if (a != null) {
192                extFnsESU = (a.getValue());
193            }
194        } catch (Exception ex) {
195            log.error("error handling decoder's extFnsESU value");
196        }
197
198        switch (extFnsESU) {
199            case "V4":
200            case "yes":
201                numFns = 29;
202                numReserved = 0;
203                numOuts = 12;
204                numLogic = 16;
205                numSounds = 24;
206                numRows = 40;
207                outBlockSiStartCv = new int[]{2, 2, 2, 2};
208                outBlockSiCvModulus = new int[]{16, 16, 16, 16};
209                outBlockStartCv = new int[]{257, 266, 268, 270};
210                outBlockCvModulus = new int[]{16, 16, 16, 16};
211
212                break;
213            case "V5":
214                numFns = 32;
215                numReserved = 1;
216                numOuts = 20;
217                numLogic = 24;
218                numSounds = 32;
219                numRows = 72;
220                outBlockSiStartCv = new int[]{3, 8, 8, 8};
221                outBlockSiCvModulus = new int[]{16, 16, 16, 16};
222                outBlockStartCv = new int[]{257, 257, 260, 263};
223                outBlockCvModulus = new int[]{16, 16, 16, 16};
224                break;
225            default:
226                throw new IllegalArgumentException("Invalid extFnsESU value '" + extFnsESU + "'");
227        }
228
229        // get relevant model attributes
230        loadModelAttributes(model);
231
232        // build outBlock*** arrays
233        outBlockLength = new int[]{numStates + numFns + numWheelSensors + numReserved + numSensors,
234            numOuts + numConfig2, numLogic, numSounds
235        };
236        outBlockItemBits = new int[]{2, 1, 1, 1};
237
238        log.debug(
239                "Constructor outBlockLength[0]={}, outBlockLength[1]={}, outBlockLength[2]={}, outBlockLength[3]={}",
240                outBlockLength[0], outBlockLength[1], outBlockLength[2], outBlockLength[3]);
241
242        maxItems = IntStream.of(outBlockLength).sum();
243        if (numItems <= 0) {
244            numItems = maxItems;
245        }
246        if (numItems > maxItems) {
247            log.error("numItems={} exceeds the maximum number of items ({}) defined in the code", numItems, maxItems);
248            numItems = Math.min(numItems, maxItems);
249        }
250
251        log.debug(
252                "Constructor numFns={}, numRows={}, numOuts={}, numItems={}, maxItems={}",
253                numFns, numRows, numOuts, numItems, maxItems);
254
255        itemName = new String[maxItems][3];
256        iVarIndex = new int[maxItems][numRows];
257        itemIsUsed = new boolean[maxItems];
258        itemLabel = new String[maxItems];
259        itemDescESU = new String[maxItems];
260        summaryLine = new JTextField[numRows][outBlockLength.length];
261        outBlockUsed = new int[outBlockLength.length];
262        outBlockStartCol = new int[outBlockLength.length];
263        outBlockName = new String[outBlockLength.length];
264
265        log.debug(
266                "ESU Function map starts");
267        varModel = v;
268
269        setupDefaultNamesLabels();
270
271        // configure numRows(from numFns), numItems(from numOuts) & any custom labels from decoder file
272        configOutputs(model);
273
274        // initialize the layout
275        gl = new GridBagLayout();
276        cs = new GridBagConstraints();
277
278        setLayout(gl);
279
280        cs.anchor = GridBagConstraints.LINE_START;
281        cs.gridwidth = GridBagConstraints.REMAINDER;
282
283        saveAt(HINTS_ROW,
284                0, new JLabel("<html><em>(For hints and instructions for using this pane, see the </em><strong>&quot;Function Map&quot;</strong><em> section of the </em><strong>&quot;Read Me - IMPORTANT&quot;</strong><em> pane.)</em><br />&nbsp;</html>"));
285        cs.gridwidth = 1;
286
287        // for row moves
288        ButtonGroup group = new ButtonGroup();
289        rowButton = new JRadioButton[numRows];
290
291        // add row move buttons
292        addRowMoveButtons();
293
294        cs.anchor = GridBagConstraints.LINE_END;
295
296        saveAt(ROW_LABEL_ROW, firstOut
297                - 1, new JLabel("Row"));
298
299        cs.anchor = GridBagConstraints.LINE_START;
300
301        int siCV = 0;
302
303        // loop through rows
304        for (int iRow = 0;
305                iRow < numRows;
306                iRow++) {
307            currentCol = firstCol;
308            int outBlockNum = -1;
309            int nextOutBlockStart = 0;
310            int thisOutBlockStart = 0;
311            int nextFreeBit = 0;
312            // add row shift buttons
313            {
314                rowButton[iRow] = new JRadioButton();
315                rowButton[iRow].setActionCommand(String.valueOf(iRow));
316                rowButton[iRow].setToolTipText(Bundle.getMessage("FnMapESURowSelect"));
317                rowButton[iRow].addActionListener(new java.awt.event.ActionListener() {
318                    @Override
319                    public void actionPerformed(java.awt.event.ActionEvent e) {
320                        selectedRow = Integer.parseInt(e.getActionCommand());
321                    }
322                });
323                group.add(rowButton[iRow]);
324                cs.anchor = GridBagConstraints.CENTER;
325                saveAt(currentRow, currentCol++, rowButton[iRow]);
326            }
327            cs.anchor = GridBagConstraints.LINE_END;
328            saveAt(currentRow, currentCol++, new JLabel(Integer.toString(iRow + 1)));
329            cs.anchor = GridBagConstraints.LINE_START;
330
331            // loop through outputs (columns)
332            int item = 0;
333            do {
334                JPanel blockPanel = new JPanel();
335                GridBagLayout blockPanelLay;
336                GridBagConstraints blockPanelCs = new GridBagConstraints();
337
338                JPanel blockItemsSelectorPanel = new JPanel();
339                GridBagLayout bIsPlay;
340                GridBagConstraints bIsPcs = new GridBagConstraints();
341
342                // check for block separators
343                if (item == nextOutBlockStart) {
344                    outBlockNum++;
345                    outBlockStartCol[outBlockNum] = item;
346                    thisOutBlockStart = item;
347                    nextOutBlockStart = item + outBlockLength[outBlockNum];
348                    blockItemsSelectorPanel = new JPanel();
349                    siCV = outBlockSiStartCv[outBlockNum] + (iRow / outBlockSiCvModulus[outBlockNum]);
350                    nextFreeBit = 0;
351
352                    bIsPlay = new GridBagLayout();
353                    bIsPcs = new GridBagConstraints();
354                    blockItemsSelectorPanel.setLayout(bIsPlay);
355                    bIsPcs.gridx = 0;
356                    bIsPcs.gridy = 0;
357
358                    blockPanelLay = new GridBagLayout();
359                    blockPanelCs = new GridBagConstraints();
360                    blockPanel.setLayout(blockPanelLay);
361                    blockPanelCs.gridx = 0;
362                    blockPanelCs.gridy = 0;
363                }
364
365                // block loop
366                do {
367                    // if column is not suppressed by blank headers
368                    if (!itemName[item][0].equals("")) {
369                        // set up the variable using the output label
370                        String name = "ESU Function Row " + Integer.toString(iRow + 1) + " Item " + Integer.toString(item + 1);
371                        int iCV = outBlockStartCv[outBlockNum] + (((outBlockCvModulus[outBlockNum] * iRow) + (nextFreeBit / BIT_MODULUS)) % CV_PAGE_MODULUS);
372                        String thisCV = PI_CV + "." + siCV + "." + iCV;
373                        int bitValue = (int) (Math.pow(2, outBlockItemBits[outBlockNum]) - 1) << (nextFreeBit % BIT_MODULUS);
374                        String bitMask = "00000000" + Integer.toBinaryString(bitValue);
375                        bitMask = (bitMask.substring(bitMask.length() - 8));
376                        String bitPattern = bitMask.replace("0", "X").replace("1", "V");
377                        log.debug("Block {} CV{} mask'{}' {}", outBlockNum, thisCV, bitPattern, name);
378
379                        // Get Cv value from file. We need to do this to get the GUI synchronised with cvModel initially
380                        int savedValue = 0;
381                        CvValue cvObject = cvModel.allCvMap().get(thisCV);
382                        if (cvObject != null) {
383                            savedValue = cvObject.getValue();
384                        }
385                        String defaultValue = Integer.toString((savedValue & bitValue) >>> (nextFreeBit % BIT_MODULUS));
386                        
387                        // skip function settings for nonexistant function outputs
388                        if (outBlockNum == 1 && item >= thisOutBlockStart + numOutsFromDefinition && item < thisOutBlockStart + numOuts) {
389                            log.debug("Skipping previous item because function output AUX {} does not exist on this decoder", item - thisOutBlockStart - 2 + 1);
390                        }
391                        else {
392                            // create a JDOM tree with some elements to add to varModel
393                            Element root = new Element("decoder-config");
394                            Document doc = new Document(root);
395                            doc.setDocType(new DocType("decoder-config"));
396
397                            // set up choices
398                            String defChoice1 = "On";
399                            String defChoice2 = "Off";
400                            if (!itemName[item][1].equals("")) {
401                                defChoice1 = itemName[item][1];
402                            }
403                            if (!itemName[item][2].equals("")) {
404                                defChoice2 = itemName[item][2];
405                            }
406
407                            // add some elements
408                            Element thisVar;
409                            if (outBlockItemBits[outBlockNum] == 2) {
410                                root.addContent(new Element("decoder") // the sites information here lists all relevant
411                                        .addContent(new Element("variables")
412                                                .addContent(thisVar = new Element("variable")
413                                                        .setAttribute("CV", thisCV)
414                                                        .setAttribute("default", defaultValue)
415                                                        .setAttribute("mask", bitPattern)
416                                                        .setAttribute("item", name)
417                                                        .setAttribute("readOnly", "no")
418                                                        .addContent(new Element("enumVal")
419                                                                .addContent(new Element("enumChoice")
420                                                                        .setAttribute("choice", "-")
421                                                                )
422                                                                .addContent(new Element("enumChoice")
423                                                                        .setAttribute("choice", defChoice1)
424                                                                )
425                                                                .addContent(new Element("enumChoice")
426                                                                        .setAttribute("choice", defChoice2)
427                                                                )
428                                                        )
429                                                )
430                                        ) // variables element
431                                ); // decoder element
432                            } else {
433                                root.addContent(new Element("decoder") // the sites information here lists all relevant
434                                        .addContent(new Element("variables")
435                                                .addContent(thisVar = new Element("variable")
436                                                        .setAttribute("CV", thisCV)
437                                                        .setAttribute("default", defaultValue)
438                                                        .setAttribute("mask", bitPattern)
439                                                        .setAttribute("item", name)
440                                                        .setAttribute("readOnly", "no")
441                                                        .addContent(new Element("enumVal")
442                                                                .addContent(new Element("enumChoice")
443                                                                        .setAttribute("choice", "Off")
444                                                                )
445                                                                .addContent(new Element("enumChoice")
446                                                                        .setAttribute("choice", "On")
447                                                                )
448                                                        )
449                                                )
450                                        ) // variables element
451                                ); // decoder element
452                            }
453                            // end of adding content
454
455                            varModel.setRow(0, thisVar);
456
457                            // cleanup
458                            root = null;
459                            doc = null;
460                            thisVar = null;
461                        }
462
463                        int iVar = varModel.findVarIndex(name, true);  // now pick up the varModel entry we just created
464
465                        // hopefully we found it!
466                        if (iVar >= 0) {
467                            // try to find item  labels for itemLabel[item]
468                            if (itemName[item][0].startsWith(Bundle.getMessage("FnMapSndSlot"))) {
469                                try {
470                                    itemLabel[item] = rosterEntry.getSoundLabel(Integer.parseInt(itemName[item][0].substring((Bundle.getMessage("FnMapSndSlot") + " ").length())));
471                                } catch (NumberFormatException e) {
472                                    log.warn("Error for sound slot label \"{}\" in \"{}\"", itemName[item][0], item);
473                                }
474                            } else if (itemName[item][0].matches("F\\d+")) {
475                                try {
476                                    int fn = Integer.parseInt(itemName[item][0].substring(1));
477                                    if (fn <= rosterEntry.getMaxFnNumAsInt()) {
478                                        itemLabel[item] = rosterEntry.getFunctionLabel(fn);
479                                    }
480                                } catch (NumberFormatException e) {
481                                    log.warn("Error for function label \"{}\" in \"{}\"", itemName[item][0], item);
482                                }
483                            }
484                            if (itemLabel[item] == null) {
485                                itemLabel[item] = "";
486                            }
487
488                            // generate a fullItemName
489                            String fullItemName = itemName[item][0];
490                            if (!itemLabel[item].equals("")) {
491                                fullItemName = fullItemName + (": " + itemLabel[item]);
492                            }
493
494                            log.debug("Process var: {} as index {}", name, iVar);
495                            varsUsed.add(iVar);
496                            JComponent varComp;
497                            if (outBlockItemBits[outBlockNum] == 1) {
498                                varComp = (JComponent) (varModel.getRep(iVar, "checkbox"));
499                            } else {
500                                varComp = (JComponent) (varModel.getRep(iVar, ""));
501                            }
502                            VariableValue var = varModel.getVariable(iVar);
503                            varComp.setToolTipText(CvUtil.addCvDescription((Bundle.getMessage("FnMapESURow") + " " + Integer.toString(iRow + 1) + ", " + fullItemName), var.getCvDescription(), var.getMask()));
504                            if (cvObject == null) {
505                                cvObject = cvModel.allCvMap().get(thisCV); // case of new loco
506                            }
507                            if (cvObject != null) {
508                                cvObject.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
509                                    private int row;
510                                    private int block;
511
512                                    @Override
513                                    public void propertyChange(java.beans.PropertyChangeEvent e) {
514                                        log.debug("Updating Summary Line for row {} block {}", row, block);
515                                        updateSummaryLine(row, block);
516                                    }
517
518                                    private java.beans.PropertyChangeListener init(int i, int j) {
519                                        row = i;
520                                        block = j;
521                                        return this;
522                                    }
523                                }.init(iRow, outBlockNum));
524                            } else {
525                                log.error("cvObject still null after attempt to allocate");
526                            }
527
528                            // add line to scroll pane
529                            String label = itemName[item][0];
530                            if (outBlockItemBits[outBlockNum] == 1) {
531                                label = fullItemName;
532                            }
533                            bIsPcs.anchor = GridBagConstraints.LINE_START;
534                            bIsPcs.gridx = outBlockItemBits[outBlockNum] % 2;
535                            blockItemsSelectorPanel.add(new JLabel(label), bIsPcs);
536                            bIsPcs.gridx = outBlockItemBits[outBlockNum] - 1;
537                            blockItemsSelectorPanel.add(varComp, bIsPcs);
538                            bIsPcs.gridy++;
539
540                            itemIsUsed[item] = true;
541                            iVarIndex[item][iRow] = iVar;
542                        } else {
543                            log.debug("Did not find var: {}", name);
544                        }
545                    }
546                    nextFreeBit = nextFreeBit + outBlockItemBits[outBlockNum];
547
548                    item++;
549                } while ((item < nextOutBlockStart) && (item < numItems)); // end block loop
550
551                // display block
552                JScrollPane blockItemsScrollPane = new JScrollPane(blockItemsSelectorPanel);
553                blockItemsScrollPane.setPreferredSize(new Dimension(400, 400));
554
555                blockPanelCs.anchor = GridBagConstraints.LINE_START;
556                blockPanelCs.gridx = 0;
557                blockPanelCs.gridy = 0;
558                blockPanelCs.insets = new Insets(0, 20, 0, 0);
559                blockPanel.add(summaryLine[iRow][outBlockNum], blockPanelCs);
560                updateSummaryLine(iRow, outBlockNum);
561
562                JButton button = new JButton("Change");
563                button.setActionCommand(iRow + "," + outBlockNum);
564                button.addActionListener(new java.awt.event.ActionListener() {
565                    @Override
566                    public void actionPerformed(java.awt.event.ActionEvent e) {
567                        String params[] = e.getActionCommand().split(",");
568                        JmriJOptionPane.showMessageDialog(
569                                blockPanel, blockItemsScrollPane, "Row " + (Integer.parseInt(params[0]) + 1) + ", "
570                                + outBlockName[Integer.parseInt(params[1])], JmriJOptionPane.PLAIN_MESSAGE);
571                    }
572                });
573                blockPanelCs.anchor = GridBagConstraints.LINE_START;
574                blockPanelCs.gridx = 1;
575                blockPanelCs.gridy = 0;
576                blockPanelCs.insets = new Insets(0, 0, 0, 0);
577                blockPanel.add(button, blockPanelCs);
578
579                saveAt(currentRow, currentCol, blockPanel);
580                currentCol++;
581
582            } while (item < numItems); // end outputs (columns) loop
583
584            saveAt(currentRow++, currentCol, new JLabel(Integer.toString(iRow + 1)));
585        }  // end row loop
586
587        saveAt(ROW_LABEL_ROW, currentCol,
588                new JLabel(Bundle.getMessage("FnMapESURow")));
589        // tally used columns
590        int currentBlock = -1;
591        int blockStart = 0;
592        for (int item = 0;
593                item < maxItems;
594                item++) {
595            if (item == blockStart) {
596                currentBlock++;
597                blockStart = blockStart + outBlockLength[currentBlock];
598                outBlockUsed[currentBlock] = 0;
599            }
600            if (itemIsUsed[item]) {
601                outBlockUsed[currentBlock]++;
602            }
603        }
604
605        // Create formatted block labels
606        for (int iBlock = 0;
607                iBlock < outBlockLength.length;
608                iBlock++) {
609            if (outBlockUsed[iBlock] > 0) {
610                StringBuilder label = new StringBuilder("<html><strong>" + outBlockName[iBlock]);
611                try {
612                    String s = Bundle.getMessage("FnMapESUBlockDesc_" + (iBlock + 1));
613                    label.append("</strong><br>");
614                    label.append(s);
615                    label.append("</html>");
616                } catch (MissingResourceException e) {
617                    label.append("</strong></html>");
618                }
619                JLabel lx = new JLabel(label.toString());
620                GridBagConstraints csx = new GridBagConstraints();
621                csx.gridy = BLOCK_NAME_ROW;
622                csx.gridx = firstOut + iBlock;
623                csx.insets = new Insets(0, 40, 0, 0);
624                csx.gridwidth = 1;
625                csx.anchor = GridBagConstraints.LINE_START;
626                gl.setConstraints(lx, csx);
627                add(lx);
628            }
629        }
630
631        log.debug(
632                "Function map complete");
633    }
634
635    /**
636     * Set up the default names and labels.
637     */
638    void setupDefaultNamesLabels() {
639        // get block names
640        for (int i = 0;
641                i < outBlockName.length;
642                i++) {
643            outBlockName[i] = Bundle.getMessage("FnMapESUBlockName_" + (i + 1));
644        }
645
646        // make item names
647        int item = 0;
648        itemDescESU[item++] = Bundle.getMessage("FnMap_STATE") + "|" + Bundle.getMessage("FnMap_DRIVE") + "|" + Bundle.getMessage("FnMap_STOP");
649        itemDescESU[item++] = Bundle.getMessage("FnMap_DIR") + "|" + Bundle.getMessage("FnMap_FWD") + "|" + Bundle.getMessage("FnMap_REV");
650        for (int i = 0; i < numFns; i++) {
651            itemDescESU[item++] = "F" + i;
652        }
653        itemDescESU[item++] = Bundle.getMessage("FnMap_WS");
654        if (extFnsESU.equalsIgnoreCase("V5")) {
655            itemDescESU[item++] = Bundle.getMessage("FnMap_RS");
656        }
657        for (int i = 1; i <= numSensors; i++) {
658            itemDescESU[item++] = Bundle.getMessage("FnMap_S") + " " + i;
659        }
660        itemDescESU[item++] = Bundle.getMessage("FnMap_HL") + "[1]";
661        itemDescESU[item++] = Bundle.getMessage("FnMap_RL") + "[1]";
662        for (int i = 1; i <= numOuts - 2; i++) {
663            itemDescESU[item++] = Bundle.getMessage("FnMap_A") + " " + i + (i <= 2 ? "[1]" : "");
664        }
665        itemDescESU[item++] = Bundle.getMessage("FnMap_HL") + "[2]";
666        itemDescESU[item++] = Bundle.getMessage("FnMap_RL") + "[2]";
667        for (int i = 1; i <= 2; i++) {
668            itemDescESU[item++] = Bundle.getMessage("FnMap_A") + " " + i + "[2]";
669        }
670        for (int i = 1; i <= outBlockLength[2]; i++) {
671            itemDescESU[item] = "(output 3," + Integer.toString(i) + ")"; // dummy, use output labels
672            item++;
673        }
674        for (int i = 1; i <= outBlockLength[3]; i++) {
675            itemDescESU[item++] = Bundle.getMessage("FnMapSndSlot") + " " + i;
676        }
677
678        // set up default names and labels
679        for (int itemNum = 0;
680                itemNum < maxItems;
681                itemNum++) {
682            itemLabel[itemNum] = "";
683            itemName[itemNum][0] = "";
684            itemName[itemNum][1] = "";
685            itemName[itemNum][2] = "";
686            itemIsUsed[itemNum] = false;
687            for (int iRow = 0; iRow < numRows; iRow++) {
688                iVarIndex[itemNum][iRow] = 0;
689                for (int outBlockNum = 0; outBlockNum < outBlockLength.length; outBlockNum++) {
690                    summaryLine[iRow][outBlockNum] = new JTextField(20);
691                    summaryLine[iRow][outBlockNum].setHorizontalAlignment(JTextField.LEFT);
692                    summaryLine[iRow][outBlockNum].setEditable(false);
693                }
694            }
695        }
696    }
697
698    /**
699     * Updates all summary lines, including setting appropriate states.
700     */
701    void updateAllSummaryLines() {
702        for (int row = 0; row < numRows; row++) {
703            for (int block = 0; block < outBlockLength.length; block++) {
704                updateSummaryLine(row, block);
705            }
706        }
707    }
708
709    /**
710     * Updates a summary line at the specified location, including setting
711     * appropriate state.
712     *
713     * @param row   the row to update
714     * @param block the block to update
715     */
716    void updateSummaryLine(int row, int block) {
717        StringBuilder retString = new StringBuilder("");
718        AbstractValue.ValueState retState = AbstractValue.ValueState.SAME;
719
720        for (int item = outBlockStartCol[block]; item < (outBlockStartCol[block] + outBlockLength[block]); item++) {
721            if (itemIsUsed[item]) {
722                int value = Integer.parseInt(varModel.getValString(iVarIndex[item][row]));
723                var state = varModel.getState(iVarIndex[item][row]);
724                if ((item == outBlockStartCol[block]) || (priorityValue(state) > priorityValue(retState))) {
725                    retState = state;
726                }
727                if (value > 0) {
728                    if (outBlockItemBits[block] == 1) {
729                        if (itemLabel[item].equals("")) {
730                            retString.append(",").append(itemName[item][0]);
731                        } else {
732                            retString.append(",").append(itemLabel[item]);
733                        }
734                    } else if (outBlockItemBits[block] == 2) {
735                        if (value > 2) {
736                            retString.append(",").append("reserved value ").append(value);
737                        } else if (itemName[item][value].equals("")) {
738                            if (value == 1) {
739                                retString.append(",").append(itemName[item][0]);
740                            } else {
741                                retString.append(",not ").append(itemName[item][0]);
742                            }
743                        } else {
744                            retString.append(",").append(itemName[item][value]);
745                        }
746                    }
747                }
748            }
749        }
750
751        if (retString.length() == 0) {
752            retString.append("-");
753        } else if (retString.charAt(0) == ',') {
754            retString.deleteCharAt(0);
755        }
756
757        summaryLine[row][block].setBackground(retState.getColor());
758        summaryLine[row][block].setText(retString.toString());
759        summaryLine[row][block].setToolTipText(retString.toString());
760    }
761
762    /**
763     * Assigns a priority value to a specified state.
764     *
765     * @param state the state
766     * @return the assigned priority value
767     */
768    @SuppressFBWarnings({"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"})
769    int priorityValue(AbstractValue.ValueState state) {
770        int value = 0;
771        switch (state) {
772            case UNKNOWN:
773                value++;
774            //$FALL-THROUGH$
775            case DIFFERENT:
776                value++;
777            //$FALL-THROUGH$
778            case EDITED:
779                value++;
780            //$FALL-THROUGH$
781            case FROMFILE:
782                value++;
783            //$FALL-THROUGH$
784            default:
785                return value;
786        }
787    }
788
789    /**
790     * Saves an item at the specified row and column.
791     *
792     * @param row    the row
793     * @param column the column
794     * @param j      the item
795     */
796    void saveAt(int row, int column, JComponent j) {
797        if (row < 0 || column < 0) {
798            return;
799        }
800        cs.gridy = row;
801        cs.gridx = column;
802        gl.setConstraints(j, cs);
803        add(j);
804    }
805
806    /**
807     * Moves rows up or down.
808     * <p>
809     * Row moves are for convenience purposes only. Decoder functioning is
810     * unaffected by row position in mapping table.
811     *
812     * @param increment number of rows to move by
813     */
814    void moveRow(int increment) {
815        if (selectedRow == -1) {
816            return;
817        }
818        if ((selectedRow + increment) < 0) {
819            return;
820        }
821        if ((selectedRow + increment) >= numRows) {
822            return;
823        }
824        int newRow = selectedRow + increment;
825        // now to swap the data
826        for (int item = 0; item < maxItems; item++) {
827            if (itemIsUsed[item]) {
828                int selectedRowValue = Integer.parseInt(varModel.getValString(iVarIndex[item][selectedRow]));
829                int newRowValue = Integer.parseInt(varModel.getValString(iVarIndex[item][newRow]));
830                varModel.setIntValue(iVarIndex[item][selectedRow], newRowValue);
831                varModel.setIntValue(iVarIndex[item][newRow], selectedRowValue);
832            }
833        }
834
835        selectedRow = newRow;
836        rowButton[selectedRow].setSelected(true);
837
838    }
839
840    /**
841     * Adds the Row Move buttons at top and bottom.
842     */
843    void addRowMoveButtons() {
844        {
845            JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowUp-16.png")));
846            button.setActionCommand(String.valueOf(-1));
847            button.setToolTipText(Bundle.getMessage("FnMapESUMoveUp"));
848//             button.setBorderPainted(false);
849            button.addActionListener(new java.awt.event.ActionListener() {
850                @Override
851                public void actionPerformed(java.awt.event.ActionEvent e) {
852                    moveRow(Integer.parseInt(e.getActionCommand()));
853                }
854            });
855            cs.anchor = GridBagConstraints.CENTER;
856            saveAt(MOVE_ARROWS_TOP_ROW, 0, button);
857        }
858        {
859            JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowDown-16.png")));
860            button.setActionCommand(String.valueOf(1));
861            button.setToolTipText(Bundle.getMessage("FnMapESUMoveDown"));
862//             button.setBorderPainted(false);
863            button.addActionListener(new java.awt.event.ActionListener() {
864                @Override
865                public void actionPerformed(java.awt.event.ActionEvent e) {
866                    moveRow(Integer.parseInt(e.getActionCommand()));
867                }
868            });
869            cs.anchor = GridBagConstraints.CENTER;
870            saveAt(MOVE_ARROWS_TOP_ROW, 1, button);
871        }
872        {
873            JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowUp-16.png")));
874            button.setActionCommand(String.valueOf(-1));
875            button.setToolTipText(Bundle.getMessage("FnMapESUMoveUp"));
876//             button.setBorderPainted(false);
877            button.addActionListener(new java.awt.event.ActionListener() {
878                @Override
879                public void actionPerformed(java.awt.event.ActionEvent e) {
880                    moveRow(Integer.parseInt(e.getActionCommand()));
881                }
882            });
883            cs.anchor = GridBagConstraints.CENTER;
884            saveAt(FIRST_ROW + numRows, 0, button);
885        }
886        {
887            JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowDown-16.png")));
888            button.setActionCommand(String.valueOf(1));
889            button.setToolTipText(Bundle.getMessage("FnMapESUMoveDown"));
890//             button.setBorderPainted(false);
891            button.addActionListener(new java.awt.event.ActionListener() {
892                @Override
893                public void actionPerformed(java.awt.event.ActionEvent e) {
894                    moveRow(Integer.parseInt(e.getActionCommand()));
895                }
896            });
897            cs.anchor = GridBagConstraints.CENTER;
898            saveAt(FIRST_ROW + numRows, 1, button);
899        }
900    }
901
902    /**
903     * Use the "model" and "family" elements from the decoder definition file to
904     * configure the number of rows and columns and set up any custom column
905     * names.
906     *
907     * @param model the "model" element from the decoder definition file
908     */
909    void configOutputs(Element model) {
910        if (model == null) {
911            log.debug("configOutputs was given a null model");
912            return;
913        }
914        Element family;
915        Parent parent = model.getParent();
916        if (parent != null && parent instanceof Element) {
917            family = (Element) parent;
918        } else {
919            log.debug("configOutputs found an invalid parent family");
920            return;
921        }
922
923        // add ESU default split labels before reading custom ones
924        for (int item = 0; item < maxItems; item++) {
925            loadSplitLabel(item, itemDescESU[item]);
926        }
927
928        // take all "output" children
929        List<Element> elemList = new ArrayList<>();
930        addOutputElements(family.getChildren(), elemList);
931        addOutputElements(model.getChildren(), elemList);
932
933        log.debug("output scan starting with {} elements", elemList.size());
934
935        for (int i = 0; i < elemList.size(); i++) {
936            Element e = elemList.get(i);
937            String name = e.getAttribute("name").getValue();
938            log.debug("output element name: {} value: {}", e.getAttributeValue("name"), e.getAttributeValue("label"));
939            // does this element have a label?
940            String label = LocaleSelector.getAttribute(e, "label");
941            if (label != null) {
942                parseLoadLabel(i, name, label);
943            }
944        }
945    }
946
947    /**
948     * Use the "model" element from the decoder definition file to fetch
949     * attributes relevant to building this function map.
950     *
951     * @param model the "model" element from the decoder definition file
952     */
953    void loadModelAttributes(Element model) {
954
955        Attribute a;
956
957        if (model == null) {
958            log.debug("configOutputs was given a null model");
959            return;
960        }
961
962        // get numOuts, numFns or leave the defaults
963        a = model.getAttribute("numFns");
964        try {
965            if (a != null) {
966                numRows = Integer.parseInt(a.getValue());
967            }
968        } catch (NumberFormatException e) {
969            log.error("error handling decoder's numFns value");
970        }
971        
972        a = model.getAttribute("numOuts");
973        try {
974            if (a != null) {
975                numOutsFromDefinition = Integer.parseInt(a.getValue());
976            }
977        } catch (NumberFormatException e) {
978            log.error("error handling decoder's numOuts value");
979        }
980        
981        log.debug("loadModelAttributes numFns={}, numRows={}, numOuts={}, numOutsFromDefinition={}, numItems={}",
982                            numFns, numRows, numOuts, numOutsFromDefinition, numItems);
983    }
984
985    /**
986     * Adds a list of "output" or "outputs" elements to an existing list.
987     *
988     * @param input      the list to add from
989     * @param accumulate the list to add to
990     */
991    void addOutputElements(List<Element> input, List<Element> accumulate) {
992        for (Element elem : input) {
993            if (elem.getName().equals("outputs")) {
994                log.debug(" found outputs element of size {}", elem.getChildren().size());
995                addOutputElements(elem.getChildren(), accumulate);
996            } else if (elem.getName().equals("output")) {
997                log.debug("adding output element name: {} value: {}", elem.getAttributeValue("name"), elem.getAttributeValue("label"));
998                accumulate.add(elem);
999            }
1000        }
1001    }
1002
1003    /**
1004     * Loads labels as per documentation at {@link FnMapPanelESU}.
1005     *
1006     * @param item  the item number to load
1007     * @param name  the "name" attribute from the "output" element
1008     * @param label the "label" attribute from the "output" element
1009     */
1010    void parseLoadLabel(int item, String name, String label) {
1011        // is the name a number?
1012        try {
1013            int outputNum = Integer.parseInt(name);
1014            // yes, since it was converted
1015            // store the label at the appropriate index
1016            log.debug("Output name='{}', label='{}' has an item number.", name, label);
1017            loadSplitLabel(outputNum - 1, label);
1018            return;
1019        } catch (java.lang.NumberFormatException ex) {
1020            log.debug("Output name='{}', label='{}' is not a simple item number.", name, label);
1021        }
1022
1023        // see if it is a "block,item" construct
1024        String[] itemList = name.split(",");
1025        if (itemList.length == 2) {
1026            try {
1027                int blockNum = Integer.parseInt(itemList[0]);
1028                int blockItemNum = Integer.parseInt(itemList[1]);
1029                int itemNum = Arrays.stream(outBlockLength, 0, blockNum - 1).sum() + blockItemNum - 1;
1030                log.debug("Output name='{}', label='{}', blockNum='{}', blockItemNum='{}', itemNum='{}'.",
1031                        name, label, blockNum, blockItemNum, itemNum);
1032                loadSplitLabel(itemNum, label);
1033                return;
1034            } catch (java.lang.NumberFormatException ex1) {
1035                log.debug("Output name='{}', label='{}' is not a \"block,item\" construct.",
1036                        name, label);
1037            }
1038        }
1039
1040        //  must be a name
1041        if (item < maxItems) {
1042            itemName[item][0] = name;
1043            itemName[item][1] = "";
1044            itemName[item][2] = "";
1045            log.debug("Output name='{}', label='{}' has no item number.", name, label);
1046            loadSplitLabel(item, name + "|" + label);
1047        }
1048    }
1049
1050    /**
1051     * Splits a label as per documentation at {@link FnMapPanelESU}.
1052     *
1053     * @param item     the item number to load
1054     * @param theLabel the label attribute from the "output" element
1055     */
1056    void loadSplitLabel(int item, String theLabel) {
1057        if (item < maxItems) {
1058            String[] itemList = theLabel.split("\\|");
1059            if (theLabel.equals("|")) {
1060                itemName[item][0] = "";
1061                itemName[item][1] = "";
1062                itemName[item][2] = "";
1063            } else if (itemList.length == 1) {
1064                itemName[item][0] = itemList[0];
1065                itemName[item][1] = "";
1066            } else if (itemList.length == 2) {
1067                itemName[item][0] = itemList[0];
1068                itemName[item][1] = itemList[1];
1069                itemName[item][2] = "";
1070            } else if (itemList.length > 2) {
1071                itemName[item][0] = itemList[0];
1072                itemName[item][1] = itemList[1];
1073                itemName[item][2] = itemList[2];
1074            }
1075        }
1076    }
1077
1078    /**
1079     * Clean up at end.
1080     */
1081    public void dispose() {
1082        varModel = null; // kills GC cycles during test
1083        for (int i = 0; i < rowButton.length; i++) {
1084            rowButton[i] = null;
1085        }
1086        for (JTextField[] summaryLine1 : summaryLine) {
1087            for (int j = 0; j < summaryLine1.length; j++) {
1088                summaryLine1[j] = null;
1089            }
1090        }
1091        removeAll();  // JPanel call
1092    }
1093
1094    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FnMapPanelESU.class);
1095
1096}