001package jmri.jmrix.openlcb.swing.stleditor;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.io.*;
006import java.util.*;
007import java.util.List;
008import java.util.concurrent.atomic.AtomicInteger;
009import java.util.regex.Pattern;
010import java.nio.file.*;
011
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014
015import javax.swing.*;
016import javax.swing.event.ChangeEvent;
017import javax.swing.event.ListSelectionEvent;
018import javax.swing.filechooser.FileNameExtensionFilter;
019import javax.swing.table.AbstractTableModel;
020
021import jmri.InstanceManager;
022import jmri.UserPreferencesManager;
023import jmri.jmrix.can.CanSystemConnectionMemo;
024import jmri.util.FileUtil;
025import jmri.util.swing.JComboBoxUtil;
026import jmri.util.swing.JmriJFileChooser;
027import jmri.util.swing.JmriJOptionPane;
028import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
029
030import static org.openlcb.MimicNodeStore.NodeMemo.UPDATE_PROP_SIMPLE_NODE_IDENT;
031
032import org.apache.commons.csv.CSVFormat;
033import org.apache.commons.csv.CSVParser;
034import org.apache.commons.csv.CSVPrinter;
035import org.apache.commons.csv.CSVRecord;
036
037import org.openlcb.*;
038import org.openlcb.cdi.cmd.*;
039import org.openlcb.cdi.impl.ConfigRepresentation;
040
041
042/**
043 * Panel for editing STL logic.
044 *
045 * The primary mode is a connection to a Tower LCC+Q.  When a node is selected, the data
046 * is transferred to Java lists and displayed using Java tables. If changes are to be retained,
047 * the Store process is invoked which updates the Tower LCC+Q CDI.
048 *
049 * An alternate mode uses CSV files to import and export the data.  This enables offline development.
050 * Since the CDI is loaded automatically when the node is selected, to transfer offline development
051 * is a three step process:  Load the CDI, replace the content with the CSV content and then store
052 * to the CDI.
053 *
054 * A third mode is to load a CDI backup file.  This can then be used with the CSV process for offline work.
055 *
056 * The reboot process has several steps.
057 * <ul>
058 *   <li>The Yes option is selected in the compile needed dialog. This sends the reboot command.</li>
059 *   <li>The RebootListener detects that the reboot is done and does getCompileMessage.</li>
060 *   <li>getCompileMessage does a reload for the first syntax message.</li>
061 *   <li>EntryListener gets the reload done event and calls displayCompileMessage.</li>
062 * </ul>
063 *
064 * @author Dave Sand Copyright (C) 2024
065 * @since 5.7.5
066 */
067public class StlEditorPane extends jmri.util.swing.JmriPanel
068        implements jmri.jmrix.can.swing.CanPanelInterface {
069
070    /**
071     * The STL Editor is dependent on the Tower LCC+Q software version
072     */
073    private static int TOWER_LCC_Q_NODE_VERSION = 109;
074    private static String TOWER_LCC_Q_NODE_VERSION_STRING = "v1.09";
075
076    private CanSystemConnectionMemo _canMemo;
077    private OlcbInterface _iface;
078    private ConfigRepresentation _cdi;
079    private MimicNodeStore _store;
080
081    /* Preferences setup */
082    final String _storeModeCheck = this.getClass().getName() + ".StoreMode";
083    private final UserPreferencesManager _pm;
084    private JCheckBox _compactOption = new JCheckBox(Bundle.getMessage("StoreMode"));
085
086    private boolean _dirty = false;
087    private int _logicRow = -1;     // The last selected row, -1 for none
088    private int _groupRow = 0;
089    private List<String> _csvMessages = new ArrayList<>();
090    private AtomicInteger _storeQueueLength = new AtomicInteger(0);
091    private boolean _compileNeeded = false;
092    private boolean _compileInProgress = false;
093    PropertyChangeListener _entryListener = new EntryListener();
094    private List<String> _messages = new ArrayList<>();
095
096    private String _csvDirectoryPath = "";
097
098    private DefaultComboBoxModel<NodeEntry> _nodeModel = new DefaultComboBoxModel<NodeEntry>();
099    private JComboBox<NodeEntry> _nodeBox;
100
101    private JComboBox<Operator> _operators = new JComboBox<>(Operator.values());
102
103    private TreeMap<Integer, Token> _tokenMap;
104
105    private List<GroupRow> _groupList = new ArrayList<>();
106    private List<InputRow> _inputList = new ArrayList<>();
107    private List<OutputRow> _outputList = new ArrayList<>();
108    private List<ReceiverRow> _receiverList = new ArrayList<>();
109    private List<TransmitterRow> _transmitterList = new ArrayList<>();
110
111    private JTable _groupTable;
112    private JTable _logicTable;
113    private JTable _inputTable;
114    private JTable _outputTable;
115    private JTable _receiverTable;
116    private JTable _transmitterTable;
117
118    private JTabbedPane _detailTabs;
119
120    private JPanel _editButtons;
121    private JButton _addButton;
122    private JButton _insertButton;
123    private JButton _moveUpButton;
124    private JButton _moveDownButton;
125    private JButton _deleteButton;
126    private JButton _percentButton;
127    private JButton _refreshButton;
128    private JButton _storeButton;
129    private JButton _exportButton;
130    private JButton _importButton;
131
132    private JMenuItem _refreshItem;
133    private JMenuItem _storeItem;
134    private JMenuItem _exportItem;
135    private JMenuItem _importItem;
136    private JMenuItem _loadItem;
137
138    // CDI Names
139    private static String INPUT_NAME = "Logic Inputs.Group I%s(%s).Input Description";
140    private static String INPUT_TRUE = "Logic Inputs.Group I%s(%s).True";
141    private static String INPUT_FALSE = "Logic Inputs.Group I%s(%s).False";
142    private static String OUTPUT_NAME = "Logic Outputs.Group Q%s(%s).Output Description";
143    private static String OUTPUT_TRUE = "Logic Outputs.Group Q%s(%s).True";
144    private static String OUTPUT_FALSE = "Logic Outputs.Group Q%s(%s).False";
145    private static String RECEIVER_NAME = "Track Receivers.Rx Circuit(%s).Remote Mast Description";
146    private static String RECEIVER_EVENT = "Track Receivers.Rx Circuit(%s).Link Address";
147    private static String TRANSMITTER_NAME = "Track Transmitters.Tx Circuit(%s).Track Circuit Description";
148    private static String TRANSMITTER_EVENT = "Track Transmitters.Tx Circuit(%s).Link Address";
149    private static String GROUP_NAME = "Conditionals.Logic(%s).Group Description";
150    private static String GROUP_MULTI_LINE = "Conditionals.Logic(%s).MultiLine";
151    private static String SYNTAX_MESSAGE = "Syntax Messages.Syntax Messages.Message 1";
152
153    // Regex Patterns
154    private static Pattern PARSE_VARIABLE = Pattern.compile("[IQYZM](\\d+)\\.(\\d+)", Pattern.CASE_INSENSITIVE);
155    private static Pattern PARSE_NOVAROPER = Pattern.compile("(A\\(|AN\\(|O\\(|ON\\(|X\\(|XN\\(|\\)|NOT|SET|CLR|SAVE)", Pattern.CASE_INSENSITIVE);
156    private static Pattern PARSE_LABEL = Pattern.compile("([a-zA-Z]\\w{0,3}:)");
157    private static Pattern PARSE_JUMP = Pattern.compile("(JNBI|JCN|JCB|JNB|JBI|JU|JC)", Pattern.CASE_INSENSITIVE);
158    private static Pattern PARSE_DEST = Pattern.compile("(\\w{1,4})");
159    private static Pattern PARSE_TIMERWORD = Pattern.compile("([W]#[0123]#\\d{1,3})", Pattern.CASE_INSENSITIVE);
160    private static Pattern PARSE_TIMERVAR = Pattern.compile("([T]\\d{1,2})", Pattern.CASE_INSENSITIVE);
161    private static Pattern PARSE_COMMENT1 = Pattern.compile("//(.*)\\n");
162    private static Pattern PARSE_COMMENT2 = Pattern.compile("/\\*(.*?)\\*/");
163    private static Pattern PARSE_HEXPAIR = Pattern.compile("^[0-9a-fA-F]{2}$");
164    private static Pattern PARSE_VERSION = Pattern.compile("^.*(\\d+)\\.(\\d+)$");
165
166
167    public StlEditorPane() {
168        _pm = InstanceManager.getDefault(UserPreferencesManager.class);
169    }
170
171    @Override
172    public void initComponents(CanSystemConnectionMemo memo) {
173        _canMemo = memo;
174        _iface = memo.get(OlcbInterface.class);
175        _store = memo.get(MimicNodeStore.class);
176
177        // Add to GUI here
178        setLayout(new BorderLayout());
179
180        var footer = new JPanel();
181        footer.setLayout(new BorderLayout());
182
183        _addButton = new JButton(Bundle.getMessage("ButtonAdd"));
184        _insertButton = new JButton(Bundle.getMessage("ButtonInsert"));
185        _moveUpButton = new JButton(Bundle.getMessage("ButtonMoveUp"));
186        _moveDownButton = new JButton(Bundle.getMessage("ButtonMoveDown"));
187        _deleteButton = new JButton(Bundle.getMessage("ButtonDelete"));
188        _percentButton = new JButton("0%");
189        _refreshButton = new JButton(Bundle.getMessage("ButtonRefresh"));
190        _storeButton = new JButton(Bundle.getMessage("ButtonStore"));
191        _exportButton = new JButton(Bundle.getMessage("ButtonExport"));
192        _importButton = new JButton(Bundle.getMessage("ButtonImport"));
193
194        _refreshButton.setEnabled(false);
195        _storeButton.setEnabled(false);
196
197        _addButton.addActionListener(this::pushedAddButton);
198        _insertButton.addActionListener(this::pushedInsertButton);
199        _moveUpButton.addActionListener(this::pushedMoveUpButton);
200        _moveDownButton.addActionListener(this::pushedMoveDownButton);
201        _deleteButton.addActionListener(this::pushedDeleteButton);
202        _percentButton.addActionListener(this::pushedPercentButton);
203        _refreshButton.addActionListener(this::pushedRefreshButton);
204        _storeButton.addActionListener(this::pushedStoreButton);
205        _exportButton.addActionListener(this::pushedExportButton);
206        _importButton.addActionListener(this::pushedImportButton);
207
208        _editButtons = new JPanel();
209        _editButtons.add(_addButton);
210        _editButtons.add(_insertButton);
211        _editButtons.add(_moveUpButton);
212        _editButtons.add(_moveDownButton);
213        _editButtons.add(_deleteButton);
214        _editButtons.add(_percentButton);
215        footer.add(_editButtons, BorderLayout.WEST);
216
217        var dataButtons = new JPanel();
218        dataButtons.add(_importButton);
219        dataButtons.add(_exportButton);
220        dataButtons.add(new JLabel(" | "));
221        dataButtons.add(_refreshButton);
222        dataButtons.add(_storeButton);
223        footer.add(dataButtons, BorderLayout.EAST);
224        add(footer, BorderLayout.SOUTH);
225
226        // Define the node selector which goes in the header
227        var nodeSelector = new JPanel();
228        nodeSelector.setLayout(new FlowLayout());
229
230        _nodeBox = new JComboBox<NodeEntry>(_nodeModel);
231
232        // Load node selector combo box
233        for (MimicNodeStore.NodeMemo nodeMemo : _store.getNodeMemos() ) {
234            newNodeInList(nodeMemo);
235        }
236
237        _nodeBox.addActionListener(this::nodeSelected);
238        JComboBoxUtil.setupComboBoxMaxRows(_nodeBox);
239
240        // Force combo box width
241        var dim = _nodeBox.getPreferredSize();
242        var newDim = new Dimension(400, (int)dim.getHeight());
243        _nodeBox.setPreferredSize(newDim);
244
245        nodeSelector.add(_nodeBox);
246
247        //Setup up store mode checkbox
248        var storeMode = new JPanel();
249        _compactOption.setToolTipText(Bundle.getMessage("StoreModeTip"));
250        _compactOption.setSelected(_pm.getSimplePreferenceState(_storeModeCheck));
251        storeMode.add(_compactOption);
252
253        var header = new JPanel();
254        header.setLayout(new BorderLayout());
255        header.add(storeMode, BorderLayout.EAST);
256        header.add(nodeSelector, BorderLayout.CENTER);
257
258        add(header, BorderLayout.NORTH);
259
260        // Define the center section of the window which consists of 5 tabs
261        _detailTabs = new JTabbedPane();
262
263        _detailTabs.add(Bundle.getMessage("ButtonG"), buildLogicPanel());  // NOI18N
264        _detailTabs.add(Bundle.getMessage("ButtonI"), buildInputPanel());  // NOI18N
265        _detailTabs.add(Bundle.getMessage("ButtonQ"), buildOutputPanel());  // NOI18N
266        _detailTabs.add(Bundle.getMessage("ButtonY"), buildReceiverPanel());  // NOI18N
267        _detailTabs.add(Bundle.getMessage("ButtonZ"), buildTransmitterPanel());  // NOI18N
268
269        _detailTabs.addChangeListener(this::tabSelected);
270
271        _detailTabs.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
272
273        add(_detailTabs, BorderLayout.CENTER);
274
275        initalizeLists();
276    }
277
278    // --------------  tab configurations ---------
279
280    private JScrollPane buildGroupPanel() {
281        // Create scroll pane
282        var model = new GroupModel();
283        _groupTable = new JTable(model);
284        var scrollPane = new JScrollPane(_groupTable);
285
286        // resize columns
287        for (int i = 0; i < model.getColumnCount(); i++) {
288            int width = model.getPreferredWidth(i);
289            _groupTable.getColumnModel().getColumn(i).setPreferredWidth(width);
290        }
291
292        _groupTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
293
294        var  selectionModel = _groupTable.getSelectionModel();
295        selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
296        selectionModel.addListSelectionListener(this::handleGroupRowSelection);
297
298        return scrollPane;
299    }
300
301    private JSplitPane buildLogicPanel() {
302        // Create scroll pane
303        var model = new LogicModel();
304        _logicTable = new JTable(model);
305        var logicScrollPane = new JScrollPane(_logicTable);
306
307        // resize columns
308        for (int i = 0; i < _logicTable.getColumnCount(); i++) {
309            int width = model.getPreferredWidth(i);
310            _logicTable.getColumnModel().getColumn(i).setPreferredWidth(width);
311        }
312
313        _logicTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
314
315        // Use the operators combo box for the operator column
316        var col = _logicTable.getColumnModel().getColumn(1);
317        col.setCellEditor(new DefaultCellEditor(_operators));
318        JComboBoxUtil.setupComboBoxMaxRows(_operators);
319
320        var  selectionModel = _logicTable.getSelectionModel();
321        selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
322        selectionModel.addListSelectionListener(this::handleLogicRowSelection);
323
324        var logicPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, buildGroupPanel(), logicScrollPane);
325        logicPanel.setDividerSize(10);
326        logicPanel.setResizeWeight(.10);
327        logicPanel.setDividerLocation(150);
328
329        return logicPanel;
330    }
331
332    private JScrollPane buildInputPanel() {
333        // Create scroll pane
334        var model = new InputModel();
335        _inputTable = new JTable(model);
336        var scrollPane = new JScrollPane(_inputTable);
337
338        // resize columns
339        for (int i = 0; i < model.getColumnCount(); i++) {
340            int width = model.getPreferredWidth(i);
341            _inputTable.getColumnModel().getColumn(i).setPreferredWidth(width);
342        }
343
344        _inputTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
345
346        return scrollPane;
347    }
348
349    private JScrollPane buildOutputPanel() {
350        // Create scroll pane
351        var model = new OutputModel();
352        _outputTable = new JTable(model);
353        var scrollPane = new JScrollPane(_outputTable);
354
355        // resize columns
356        for (int i = 0; i < model.getColumnCount(); i++) {
357            int width = model.getPreferredWidth(i);
358            _outputTable.getColumnModel().getColumn(i).setPreferredWidth(width);
359        }
360
361        _outputTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
362
363        return scrollPane;
364    }
365
366    private JScrollPane buildReceiverPanel() {
367        // Create scroll pane
368        var model = new ReceiverModel();
369        _receiverTable = new JTable(model);
370        var scrollPane = new JScrollPane(_receiverTable);
371
372        // resize columns
373        for (int i = 0; i < model.getColumnCount(); i++) {
374            int width = model.getPreferredWidth(i);
375            _receiverTable.getColumnModel().getColumn(i).setPreferredWidth(width);
376        }
377
378        _receiverTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
379
380        return scrollPane;
381    }
382
383    private JScrollPane buildTransmitterPanel() {
384        // Create scroll pane
385        var model = new TransmitterModel();
386        _transmitterTable = new JTable(model);
387        var scrollPane = new JScrollPane(_transmitterTable);
388
389        // resize columns
390        for (int i = 0; i < model.getColumnCount(); i++) {
391            int width = model.getPreferredWidth(i);
392            _transmitterTable.getColumnModel().getColumn(i).setPreferredWidth(width);
393        }
394
395        _transmitterTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
396
397        return scrollPane;
398    }
399
400    private void tabSelected(ChangeEvent e) {
401        if (_detailTabs.getSelectedIndex() == 0) {
402            _editButtons.setVisible(true);
403        } else {
404            _editButtons.setVisible(false);
405        }
406    }
407
408    // --------------  Initialization ---------
409
410    private void initalizeLists() {
411        // Group List
412        for (int i = 0; i < 16; i++) {
413            _groupList.add(new GroupRow(""));
414        }
415
416        // Input List
417        for (int i = 0; i < 128; i++) {
418            _inputList.add(new InputRow("", "", ""));
419        }
420
421        // Output List
422        for (int i = 0; i < 128; i++) {
423            _outputList.add(new OutputRow("", "", ""));
424        }
425
426        // Receiver List
427        for (int i = 0; i < 16; i++) {
428            _receiverList.add(new ReceiverRow("", ""));
429        }
430
431        // Transmitter List
432        for (int i = 0; i < 16; i++) {
433            _transmitterList.add(new TransmitterRow("", ""));
434        }
435    }
436
437    // --------------  Logic table methods ---------
438
439    private void handleGroupRowSelection(ListSelectionEvent e) {
440        if (!e.getValueIsAdjusting()) {
441            _groupRow = _groupTable.getSelectedRow();
442            _logicTable.revalidate();
443            _logicTable.repaint();
444            pushedPercentButton(null);
445        }
446    }
447
448    private void pushedPercentButton(ActionEvent e) {
449        encode(_groupList.get(_groupRow));
450        _percentButton.setText(_groupList.get(_groupRow).getSize());
451    }
452
453    private void handleLogicRowSelection(ListSelectionEvent e) {
454        if (!e.getValueIsAdjusting()) {
455            _logicRow = _logicTable.getSelectedRow();
456            _moveUpButton.setEnabled(_logicRow > 0);
457            _moveDownButton.setEnabled(_logicRow < _logicTable.getRowCount() - 1);
458        }
459    }
460
461    private void pushedAddButton(ActionEvent e) {
462        var logicList = _groupList.get(_groupRow).getLogicList();
463        logicList.add(new LogicRow("", null, "", ""));
464        _logicRow = logicList.size() - 1;
465        _logicTable.revalidate();
466        _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
467        setDirty(true);
468    }
469
470    private void pushedInsertButton(ActionEvent e) {
471        var logicList = _groupList.get(_groupRow).getLogicList();
472        if (_logicRow >= 0 && _logicRow < logicList.size()) {
473            logicList.add(_logicRow, new LogicRow("", null, "", ""));
474            _logicTable.revalidate();
475            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
476        }
477        setDirty(true);
478    }
479
480    private void pushedMoveUpButton(ActionEvent e) {
481        var logicList = _groupList.get(_groupRow).getLogicList();
482        if (_logicRow >= 0 && _logicRow < logicList.size()) {
483            var logicRow = logicList.remove(_logicRow);
484            logicList.add(_logicRow - 1, logicRow);
485            _logicRow--;
486            _logicTable.revalidate();
487            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
488        }
489        setDirty(true);
490    }
491
492    private void pushedMoveDownButton(ActionEvent e) {
493        var logicList = _groupList.get(_groupRow).getLogicList();
494        if (_logicRow >= 0 && _logicRow < logicList.size()) {
495            var logicRow = logicList.remove(_logicRow);
496            logicList.add(_logicRow + 1, logicRow);
497            _logicRow++;
498            _logicTable.revalidate();
499            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
500        }
501        setDirty(true);
502    }
503
504    private void pushedDeleteButton(ActionEvent e) {
505        var logicList = _groupList.get(_groupRow).getLogicList();
506        if (_logicRow >= 0 && _logicRow < logicList.size()) {
507            logicList.remove(_logicRow);
508            _logicTable.revalidate();
509        }
510        setDirty(true);
511    }
512
513    // --------------  Encode/Decode methods ---------
514
515    private String nameToVariable(String name) {
516        if (name != null && !name.isEmpty()) {
517            if (!name.contains("~")) {
518                // Search input and output tables
519                for (int i = 0; i < 16; i++) {
520                    for (int j = 0; j < 8; j++) {
521                        int row = (i * 8) + j;
522                        if (_inputList.get(row).getName().equals(name)) {
523                            return "I" + i + "." + j;
524                        }
525                    }
526                }
527
528                for (int i = 0; i < 16; i++) {
529                    for (int j = 0; j < 8; j++) {
530                        int row = (i * 8) + j;
531                        if (_outputList.get(row).getName().equals(name)) {
532                            return "Q" + i + "." + j;
533                        }
534                    }
535                }
536                return name;
537
538            } else {
539                // Search receiver and transmitter tables
540                var splitName = name.split("~");
541                var baseName = splitName[0];
542                var aspectName = splitName[1];
543                var aspectNumber = 0;
544                try {
545                    aspectNumber = Integer.parseInt(aspectName);
546                    if (aspectNumber < 0 || aspectNumber > 7) {
547                        warningDialog(Bundle.getMessage("TitleAspect"), Bundle.getMessage("MessageAspect", aspectNumber));
548                        aspectNumber = 0;
549                    }
550                } catch (NumberFormatException e) {
551                    warningDialog(Bundle.getMessage("TitleAspect"), Bundle.getMessage("MessageAspect", aspectName));
552                    aspectNumber = 0;
553                }
554                for (int i = 0; i < 16; i++) {
555                    if (_receiverList.get(i).getName().equals(baseName)) {
556                        return "Y" + i + "." + aspectNumber;
557                    }
558                }
559
560                for (int i = 0; i < 16; i++) {
561                    if (_transmitterList.get(i).getName().equals(baseName)) {
562                        return "Z" + i + "." + aspectNumber;
563                    }
564                }
565                return name;
566            }
567        }
568
569        return null;
570    }
571
572    private String variableToName(String variable) {
573        String name = variable;
574
575        if (variable.length() > 1) {
576            var varType = variable.substring(0, 1);
577            var match = PARSE_VARIABLE.matcher(variable);
578            if (match.find() && match.groupCount() == 2) {
579                int first = -1;
580                int second = -1;
581                int row = -1;
582
583                try {
584                    first = Integer.parseInt(match.group(1));
585                    second = Integer.parseInt(match.group(2));
586                } catch (NumberFormatException e) {
587                    warningDialog(Bundle.getMessage("TitleVariable"), Bundle.getMessage("MessageVariable", variable));
588                    return name;
589                }
590
591                switch (varType) {
592                    case "I":
593                        row = (first * 8) + second;
594                        name = _inputList.get(row).getName();
595                        if (name.isEmpty()) {
596                            name = variable;
597                        }
598                        break;
599                    case "Q":
600                        row = (first * 8) + second;
601                        name = _outputList.get(row).getName();
602                        if (name.isEmpty()) {
603                            name = variable;
604                        }
605                        break;
606                    case "Y":
607                        row = first;
608                        name = _receiverList.get(row).getName() + "~" + second;
609                        break;
610                    case "Z":
611                        row = first;
612                        name = _transmitterList.get(row).getName() + "~" + second;
613                        break;
614                    default:
615                        log.error("Variable '{}' has an invalid first letter (IQYZ)", variable);
616               }
617            }
618        }
619
620        return name;
621    }
622
623    private void encode(GroupRow groupRow) {
624        String longLine = "";
625        String separator = (_compactOption.isSelected()) ? "" : " ";
626
627        var logicList = groupRow.getLogicList();
628        for (var row : logicList) {
629            var sb = new StringBuilder();
630            var jumpLabel = false;
631
632            if (!row.getLabel().isEmpty()) {
633                sb.append(row.getLabel() + " ");
634            }
635
636            if (row.getOper() != null) {
637                var oper = row.getOper();
638                var operName = oper.name();
639
640                // Fix special enums
641                if (operName.equals("Cp")) {
642                    operName = ")";
643                } else if (operName.equals("EQ")) {
644                    operName = "=";
645                } else if (operName.contains("p")) {
646                    operName = operName.replace("p", "(");
647                }
648
649                if (operName.startsWith("J")) {
650                    jumpLabel =true;
651                }
652                sb.append(operName);
653            }
654
655            if (!row.getName().isEmpty()) {
656                var name = row.getName().trim();
657
658                if (jumpLabel) {
659                    sb.append(" " + name + " ");
660                    jumpLabel = false;
661                } else if (isMemory(name)) {
662                    sb.append(separator + name);
663                } else if (isTimerWord(name)) {
664                    sb.append(separator + name);
665                } else if (isTimerVar(name)) {
666                    sb.append(separator + name);
667                } else {
668                    var variable = nameToVariable(name);
669                    if (variable == null) {
670                        JmriJOptionPane.showMessageDialog(null,
671                                Bundle.getMessage("MessageBadName", groupRow.getName(), name),
672                                Bundle.getMessage("TitleBadName"),
673                                JmriJOptionPane.ERROR_MESSAGE);
674                        log.error("bad name: {}", name);
675                    } else {
676                        sb.append(separator + variable);
677                    }
678                }
679            }
680
681            if (!row.getComment().isEmpty()) {
682                var comment = row.getComment().trim();
683                sb.append(separator + "//" + separator + comment);
684            }
685
686            sb.append("\n");
687
688            longLine = longLine + sb.toString();
689        }
690
691        log.debug("MultiLine: {}", longLine);
692
693        if (longLine.length() < 256) {
694            groupRow.setMultiLine(longLine);
695        } else {
696            var overflow = longLine.substring(255);
697            JmriJOptionPane.showMessageDialog(null,
698                    Bundle.getMessage("MessageOverflow", groupRow.getName(), overflow),
699                    Bundle.getMessage("TitleOverflow"),
700                    JmriJOptionPane.ERROR_MESSAGE);
701            log.error("The line overflowed, content truncated:  {}", overflow);
702        }
703    }
704
705    private boolean isMemory(String name) {
706        var match = PARSE_VARIABLE.matcher(name);
707        return (match.find() && name.startsWith("M"));
708    }
709
710    private boolean isTimerWord(String name) {
711        var match = PARSE_TIMERWORD.matcher(name);
712        return match.find();
713    }
714
715    private boolean isTimerVar(String name) {
716        var match = PARSE_TIMERVAR.matcher(name);
717        return match.find();
718    }
719
720    /**
721     * After the token tree map has been created, build the rows for the STL display.
722     * Each row has an optional label, a required operator, a name as needed and an optional comment.
723     * The operator is always required.  The other fields are added as needed.
724     * The label is found by looking at the previous token.
725     * The name is usually the next token.  If there is no name, it might be a comment.
726     * @param group The CDI group.
727     */
728    private void decode(GroupRow group) {
729        createTokenMap(group);
730
731        // Get the operator tokens.  They are the anchors for the other values.
732        for (Token token : _tokenMap.values()) {
733            if (token.getType().equals("Oper")) {
734
735                var label = "";
736                var name = "";
737                var comment = "";
738                Operator oper = getEnum(token.getName());
739
740                // Check for a label
741                var prevKey = _tokenMap.lowerKey(token.getStart());
742                if (prevKey != null) {
743                    var prevToken = _tokenMap.get(prevKey);
744                    if (prevToken.getType().equals("Label")) {
745                        label = prevToken.getName();
746                    }
747                }
748
749                // Get the name and comment
750                var nextKey = _tokenMap.higherKey(token.getStart());
751                if (nextKey != null) {
752                    var nextToken = _tokenMap.get(nextKey);
753
754                    if (nextToken.getType().equals("Comment")) {
755                        // There is no name between the operator and the comment
756                        comment = variableToName(nextToken.getName());
757                    } else {
758                        if (!nextToken.getType().equals("Label") &&
759                                !nextToken.getType().equals("Oper")) {
760                            // Set the name value
761                            name = variableToName(nextToken.getName());
762
763                            // Look for comment after the name
764                            var comKey = _tokenMap.higherKey(nextKey);
765                            if (comKey != null) {
766                                var comToken = _tokenMap.get(comKey);
767                                if (comToken.getType().equals("Comment")) {
768                                    comment = comToken.getName();
769                                }
770                            }
771                        }
772                    }
773                }
774
775                var logic = new LogicRow(label, oper, name, comment);
776                group.getLogicList().add(logic);
777            }
778        }
779
780    }
781
782    /**
783     * Create a map of the tokens in the MultiLine string.  The map key contains the offset for each
784     * token in the string.  The tokens are identified using multiple passes of regex tests.
785     * <ol>
786     * <li>Find the labels which consist of 1 to 4 characters and a colon.</li>
787     * <li>Find the table references.  These are the IQYZM tables.  The related operators are found by parsing backwards.</li>
788     * <li>Find the operators that do not have operands.  Note: This might include SETn. These wil be fixed when the timers are processed</li>
789     * <li>Find the jump operators and the jump destinations.</li>
790     * <li>Find the timer word and load operator.</li>
791     * <li>Find timer variable locations and Sx operators.  The SE Tn will update the SET token with the same offset. </li>
792     * <li>Find //...nl comments.</li>
793     * <li>Find /&#42;...&#42;/ comments.</li>
794     * </ol>
795     * An additional check looks for overlaps between jump destinations and labels.  This can occur when
796     * a using the compact mode, a jump destination has less the 4 characters, and is immediatly followed by a label.
797     * @param group The CDI group.
798     */
799    private void createTokenMap(GroupRow group) {
800        _messages.clear();
801        _tokenMap = new TreeMap<>();
802        var line = group.getMultiLine();
803
804        // Find label locations
805        var matchLabel = PARSE_LABEL.matcher(line);
806        while (matchLabel.find()) {
807            var label = line.substring(matchLabel.start(), matchLabel.end());
808            _tokenMap.put(matchLabel.start(), new Token("Label", label, matchLabel.start(), matchLabel.end()));
809        }
810
811        // Find variable locations and operators
812        var matchVar = PARSE_VARIABLE.matcher(line);
813        while (matchVar.find()) {
814            var variable = line.substring(matchVar.start(), matchVar.end());
815            _tokenMap.put(matchVar.start(), new Token("Var", variable, matchVar.start(), matchVar.end()));
816            var operToken = findOperator(matchVar.start() - 1, line);
817            if (operToken != null) {
818                _tokenMap.put(operToken.getStart(), operToken);
819            }
820        }
821
822        // Find operators without variables
823        var matchOper = PARSE_NOVAROPER.matcher(line);
824        while (matchOper.find()) {
825            var oper = line.substring(matchOper.start(), matchOper.end());
826
827            if (isOperInComment(line, matchOper.start())) {
828                continue;
829            }
830
831            if (getEnum(oper) != null) {
832                _tokenMap.put(matchOper.start(), new Token("Oper", oper, matchOper.start(), matchOper.end()));
833            } else {
834                _messages.add(Bundle.getMessage("ErrStandAlone", oper));
835            }
836        }
837
838        // Find jump operators and destinations
839        var matchJump = PARSE_JUMP.matcher(line);
840        while (matchJump.find()) {
841            var jump = line.substring(matchJump.start(), matchJump.end());
842            if (getEnum(jump) != null && (jump.startsWith("J") || jump.startsWith("j"))) {
843                _tokenMap.put(matchJump.start(), new Token("Oper", jump, matchJump.start(), matchJump.end()));
844
845                // Get the jump destination
846                var matchDest = PARSE_DEST.matcher(line);
847                if (matchDest.find(matchJump.end())) {
848                    var dest = matchDest.group(1);
849                    _tokenMap.put(matchDest.start(), new Token("Dest", dest, matchDest.start(), matchDest.end()));
850                } else {
851                    _messages.add(Bundle.getMessage("ErrJumpDest", jump));
852                }
853            } else {
854                _messages.add(Bundle.getMessage("ErrJumpOper", jump));
855            }
856        }
857
858        // Find timer word locations and load operator
859        var matchTimerWord = PARSE_TIMERWORD.matcher(line);
860        while (matchTimerWord.find()) {
861            var timerWord = matchTimerWord.group(1);
862            _tokenMap.put(matchTimerWord.start(), new Token("TimerWord", timerWord, matchTimerWord.start(), matchTimerWord.end()));
863            var operToken = findOperator(matchTimerWord.start() - 1, line);
864            if (operToken != null) {
865                if (operToken.getName().equals("L") || operToken.getName().equals("l")) {
866                    _tokenMap.put(operToken.getStart(), operToken);
867                } else {
868                    _messages.add(Bundle.getMessage("ErrTimerLoad", operToken.getName()));
869                }
870            }
871        }
872
873        // Find timer variable locations and S operators
874        var matchTimerVar = PARSE_TIMERVAR.matcher(line);
875        while (matchTimerVar.find()) {
876            var timerVar = matchTimerVar.group(1);
877            _tokenMap.put(matchTimerVar.start(), new Token("TimerVar", timerVar, matchTimerVar.start(), matchTimerVar.end()));
878            var operToken = findOperator(matchTimerVar.start() - 1, line);
879            if (operToken != null) {
880                _tokenMap.put(operToken.getStart(), operToken);
881            }
882        }
883
884        // Find comment locations
885        var matchComment1 = PARSE_COMMENT1.matcher(line);
886        while (matchComment1.find()) {
887            var comment = matchComment1.group(1).trim();
888            _tokenMap.put(matchComment1.start(), new Token("Comment", comment, matchComment1.start(), matchComment1.end()));
889        }
890
891        var matchComment2 = PARSE_COMMENT2.matcher(line);
892        while (matchComment2.find()) {
893            var comment = matchComment2.group(1).trim();
894            _tokenMap.put(matchComment2.start(), new Token("Comment", comment, matchComment2.start(), matchComment2.end()));
895        }
896
897        // Check for overlapping jump destinations and following labels
898        for (Token token : _tokenMap.values()) {
899            if (token.getType().equals("Dest")) {
900                var nextKey = _tokenMap.higherKey(token.getStart());
901                if (nextKey != null) {
902                    var nextToken = _tokenMap.get(nextKey);
903                    if (nextToken.getType().equals("Label")) {
904                        if (token.getEnd() > nextToken.getStart()) {
905                            _messages.add(Bundle.getMessage("ErrDestLabel", token.getName(), nextToken.getName()));
906                        }
907                    }
908                }
909            }
910        }
911
912        if (_messages.size() > 0) {
913            // Display messages
914            String msgs = _messages.stream().collect(java.util.stream.Collectors.joining("\n"));
915            JmriJOptionPane.showMessageDialog(null,
916                    Bundle.getMessage("MsgParseErr", group.getName(), msgs),
917                    Bundle.getMessage("TitleParseErr"),
918                    JmriJOptionPane.ERROR_MESSAGE);
919            _messages.forEach((msg) -> {
920                log.error(msg);
921            });
922        }
923
924        // Create token debugging output
925        if (log.isDebugEnabled()) {
926            log.info("Line = {}", line);
927            for (Token token : _tokenMap.values()) {
928                log.info("Token = {}", token);
929            }
930        }
931    }
932
933    /**
934     * Starting as the operator location minus one, work backwards to find a valid operator. When
935     * one is found, create and return the token object.
936     * @param index The current location in the line.
937     * @param line The line for the current group.
938     * @return a token or null.
939     */
940    private Token findOperator(int index, String line) {
941        var sb = new StringBuilder();
942        int limit = 10;
943
944        while (limit > 0 && index >= 0) {
945            var ch = line.charAt(index);
946            if (ch != ' ') {
947                sb.insert(0, ch);
948                if (getEnum(sb.toString()) != null) {
949                    String oper = sb.toString();
950                    return new Token("Oper", oper, index, index + oper.length());
951                }
952            }
953            limit--;
954            index--;
955        }
956        _messages.add(Bundle.getMessage("ErrNoOper", index, line));
957        return null;
958    }
959
960    /**
961     * Look backwards in the line for the beginning of a comment.  This is not a precise check.
962     * @param line The line that contains the Operator.
963     * @param index The offset of the operator.
964     * @return true if the operator appears to be in a comment.
965     */
966    private boolean isOperInComment(String line, int index) {
967        int limit = 20;     // look back 20 characters
968        char previous = 0;
969
970        while (limit > 0 && index >= 0) {
971            var ch = line.charAt(index);
972
973            if (ch == 10) {
974                // Found the end of a previous statement, new line character.
975                return false;
976            }
977
978            if (ch == '*' && previous == '/') {
979                // Found the end of a previous /*...*/ comment
980                return false;
981            }
982
983            if (ch == '/' && (previous == '/' || previous == '*')) {
984                // Found the start of a comment
985                return true;
986            }
987
988            previous = ch;
989            index--;
990            limit--;
991        }
992        return false;
993    }
994
995    private Operator getEnum(String name) {
996        try {
997            var temp = name.toUpperCase();
998            if (name.equals("=")) {
999                temp = "EQ";
1000            } else if (name.equals(")")) {
1001                temp = "Cp";
1002            } else if (name.endsWith("(")) {
1003                temp = name.toUpperCase().replace("(", "p");
1004            }
1005
1006            Operator oper = Enum.valueOf(Operator.class, temp);
1007            return oper;
1008        } catch (IllegalArgumentException ex) {
1009            return null;
1010        }
1011    }
1012
1013    // --------------  node methods ---------
1014
1015    private void nodeSelected(ActionEvent e) {
1016        NodeEntry node = (NodeEntry) _nodeBox.getSelectedItem();
1017        node.getNodeMemo().addPropertyChangeListener(new RebootListener());
1018        log.debug("nodeSelected: {}", node);
1019
1020        if (isValidNodeVersionNumber(node.getNodeMemo())) {
1021            _cdi = _iface.getConfigForNode(node.getNodeID());
1022            if (_cdi.getRoot() != null) {
1023                loadCdiData();
1024            } else {
1025                JmriJOptionPane.showMessageDialogNonModal(this,
1026                        Bundle.getMessage("MessageCdiLoad", node),
1027                        Bundle.getMessage("TitleCdiLoad"),
1028                        JmriJOptionPane.INFORMATION_MESSAGE,
1029                        null);
1030                _cdi.addPropertyChangeListener(new CdiListener());
1031            }
1032        }
1033    }
1034
1035    public class CdiListener implements PropertyChangeListener {
1036        public void propertyChange(PropertyChangeEvent e) {
1037            String propertyName = e.getPropertyName();
1038            log.debug("CdiListener event = {}", propertyName);
1039
1040            if (propertyName.equals("UPDATE_CACHE_COMPLETE")) {
1041                Window[] windows = Window.getWindows();
1042                for (Window window : windows) {
1043                    if (window instanceof JDialog) {
1044                        JDialog dialog = (JDialog) window;
1045                        if (Bundle.getMessage("TitleCdiLoad").equals(dialog.getTitle())) {
1046                            dialog.dispose();
1047                        }
1048                    }
1049                }
1050                loadCdiData();
1051            }
1052        }
1053    }
1054
1055    /**
1056     * Listens for a property change that implies a node has been rebooted.
1057     * This occurs when the user has selected that the editor should do the reboot to compile the updated logic.
1058     * When the updateSimpleNodeIdent event occurs and the compile is in progress it starts the message display process.
1059     */
1060    public class RebootListener implements PropertyChangeListener {
1061        public void propertyChange(PropertyChangeEvent e) {
1062            String propertyName = e.getPropertyName();
1063            if (_compileInProgress && propertyName.equals("updateSimpleNodeIdent")) {
1064                log.debug("The reboot appears to be done");
1065                getCompileMessage();
1066            }
1067        }
1068    }
1069
1070    private void newNodeInList(MimicNodeStore.NodeMemo nodeMemo) {
1071        // Filter for Tower LCC+Q
1072        NodeID node = nodeMemo.getNodeID();
1073        String id = node.toString();
1074        log.debug("node id: {}", id);
1075        if (!id.startsWith("02.01.57.4")) {
1076            return;
1077        }
1078
1079        int i = 0;
1080        if (_nodeModel.getIndexOf(nodeMemo.getNodeID()) >= 0) {
1081            // already exists. Do nothing.
1082            return;
1083        }
1084        NodeEntry e = new NodeEntry(nodeMemo);
1085
1086        while ((i < _nodeModel.getSize()) && (_nodeModel.getElementAt(i).compareTo(e) < 0)) {
1087            ++i;
1088        }
1089        _nodeModel.insertElementAt(e, i);
1090    }
1091
1092    private boolean isValidNodeVersionNumber(MimicNodeStore.NodeMemo nodeMemo) {
1093        SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
1094        String versionString = ident.getSoftwareVersion();
1095
1096        int version = 0;
1097        var match = PARSE_VERSION.matcher(versionString);
1098        if (match.find()) {
1099            var major = match.group(1);
1100            var minor = match.group(2);
1101            version = Integer.parseInt(major + minor);
1102        }
1103
1104        if (version < TOWER_LCC_Q_NODE_VERSION) {
1105            JmriJOptionPane.showMessageDialog(null,
1106                    Bundle.getMessage("MessageVersion",
1107                            nodeMemo.getNodeID(),
1108                            versionString,
1109                            TOWER_LCC_Q_NODE_VERSION_STRING),
1110                    Bundle.getMessage("TitleVersion"),
1111                    JmriJOptionPane.WARNING_MESSAGE);
1112            return false;
1113        }
1114
1115        return true;
1116    }
1117
1118    public class EntryListener implements PropertyChangeListener {
1119        public void propertyChange(PropertyChangeEvent e) {
1120            String propertyName = e.getPropertyName();
1121            log.debug("EntryListener event = {}", propertyName);
1122
1123            if (propertyName.equals("PENDING_WRITE_COMPLETE")) {
1124                int currentLength = _storeQueueLength.decrementAndGet();
1125                log.debug("Listener: queue length = {}, source = {}", currentLength, e.getSource());
1126
1127                var entry = (ConfigRepresentation.CdiEntry) e.getSource();
1128                entry.removePropertyChangeListener(_entryListener);
1129
1130                if (currentLength < 1) {
1131                    log.debug("The queue is back to zero which implies the updates are done");
1132                    displayStoreDone();
1133                }
1134            }
1135
1136            if (_compileInProgress && propertyName.equals("UPDATE_ENTRY_DATA")) {
1137                // The refresh of the first syntax message has completed.
1138                var entry = (ConfigRepresentation.StringEntry) e.getSource();
1139                entry.removePropertyChangeListener(_entryListener);
1140                displayCompileMessage(entry.getValue());
1141            }
1142        }
1143    }
1144
1145    private void displayStoreDone() {
1146        _csvMessages.add(Bundle.getMessage("StoreDone"));
1147        var msgType = JmriJOptionPane.ERROR_MESSAGE;
1148        if (_csvMessages.size() == 1) {
1149            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
1150        }
1151        JmriJOptionPane.showMessageDialog(this,
1152                String.join("\n", _csvMessages),
1153                Bundle.getMessage("TitleCdiStore"),
1154                msgType);
1155
1156        if (_compileNeeded) {
1157            log.debug("Display compile needed message");
1158
1159            String[] options = {Bundle.getMessage("EditorReboot"), Bundle.getMessage("CdiReboot")};
1160            int response = JmriJOptionPane.showOptionDialog(this,
1161                    Bundle.getMessage("MessageCdiReboot"),
1162                    Bundle.getMessage("TitleCdiReboot"),
1163                    JmriJOptionPane.YES_NO_OPTION,
1164                    JmriJOptionPane.QUESTION_MESSAGE,
1165                    null,
1166                    options,
1167                    options[0]);
1168
1169            if (response == JmriJOptionPane.YES_OPTION) {
1170                // Set the compile in process and request the reboot.  The completion will be
1171                // handed by the RebootListener.
1172                _compileInProgress = true;
1173                _cdi.getConnection().getDatagramService().
1174                        sendData(_cdi.getRemoteNodeID(), new int[] {0x20, 0xA9});
1175            }
1176        }
1177    }
1178
1179    /**
1180     * Get the first syntax message entry, add the entry listener and request a reload (refresh).
1181     * The EntryListener will handle the reload event.
1182     */
1183    private void getCompileMessage() {
1184            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(SYNTAX_MESSAGE);
1185            entry.addPropertyChangeListener(_entryListener);
1186            entry.reload();
1187    }
1188
1189    /**
1190     * Turn off the compile in progress and display the syntax message.
1191     * @param message The first syntax message.
1192     */
1193    private void displayCompileMessage(String message) {
1194        _compileInProgress = false;
1195        JmriJOptionPane.showMessageDialog(this,
1196                Bundle.getMessage("MessageCompile", message),
1197                Bundle.getMessage("TitleCompile"),
1198                JmriJOptionPane.INFORMATION_MESSAGE);
1199    }
1200
1201    // Notifies that the contents of a given entry have changed. This will delete and re-add the
1202    // entry to the model, forcing a refresh of the box.
1203    public void updateComboBoxModelEntry(NodeEntry nodeEntry) {
1204        int idx = _nodeModel.getIndexOf(nodeEntry.getNodeID());
1205        if (idx < 0) {
1206            return;
1207        }
1208        NodeEntry last = _nodeModel.getElementAt(idx);
1209        if (last != nodeEntry) {
1210            // not the same object -- we're talking about an abandoned entry.
1211            nodeEntry.dispose();
1212            return;
1213        }
1214        NodeEntry sel = (NodeEntry) _nodeModel.getSelectedItem();
1215        _nodeModel.removeElementAt(idx);
1216        _nodeModel.insertElementAt(nodeEntry, idx);
1217        _nodeModel.setSelectedItem(sel);
1218    }
1219
1220    protected static class NodeEntry implements Comparable<NodeEntry>, PropertyChangeListener {
1221        final MimicNodeStore.NodeMemo nodeMemo;
1222        String description = "";
1223
1224        NodeEntry(MimicNodeStore.NodeMemo memo) {
1225            this.nodeMemo = memo;
1226            memo.addPropertyChangeListener(this);
1227            updateDescription();
1228        }
1229
1230        /**
1231         * Constructor for prototype display value
1232         *
1233         * @param description prototype display value
1234         */
1235        public NodeEntry(String description) {
1236            this.nodeMemo = null;
1237            this.description = description;
1238        }
1239
1240        public NodeID getNodeID() {
1241            return nodeMemo.getNodeID();
1242        }
1243
1244        MimicNodeStore.NodeMemo getNodeMemo() {
1245            return nodeMemo;
1246        }
1247
1248        private void updateDescription() {
1249            SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
1250            StringBuilder sb = new StringBuilder();
1251            sb.append(nodeMemo.getNodeID().toString());
1252
1253            addToDescription(ident.getUserName(), sb);
1254            addToDescription(ident.getUserDesc(), sb);
1255            if (!ident.getMfgName().isEmpty() || !ident.getModelName().isEmpty()) {
1256                addToDescription(ident.getMfgName() + " " +ident.getModelName(), sb);
1257            }
1258            addToDescription(ident.getSoftwareVersion(), sb);
1259            String newDescription = sb.toString();
1260            if (!description.equals(newDescription)) {
1261                description = newDescription;
1262            }
1263        }
1264
1265        private void addToDescription(String s, StringBuilder sb) {
1266            if (!s.isEmpty()) {
1267                sb.append(" - ");
1268                sb.append(s);
1269            }
1270        }
1271
1272        private long reorder(long n) {
1273            return (n < 0) ? Long.MAX_VALUE - n : Long.MIN_VALUE + n;
1274        }
1275
1276        @Override
1277        public int compareTo(NodeEntry otherEntry) {
1278            long l1 = reorder(getNodeID().toLong());
1279            long l2 = reorder(otherEntry.getNodeID().toLong());
1280            return Long.compare(l1, l2);
1281        }
1282
1283        @Override
1284        public String toString() {
1285            return description;
1286        }
1287
1288        @Override
1289        @SuppressFBWarnings(value = "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS",
1290                justification = "Purposefully attempting lookup using NodeID argument in model " +
1291                        "vector.")
1292        public boolean equals(Object o) {
1293            if (o instanceof NodeEntry) {
1294                return getNodeID().equals(((NodeEntry) o).getNodeID());
1295            }
1296            if (o instanceof NodeID) {
1297                return getNodeID().equals(o);
1298            }
1299            return false;
1300        }
1301
1302        @Override
1303        public int hashCode() {
1304            return getNodeID().hashCode();
1305        }
1306
1307        @Override
1308        public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
1309            //log.warning("Received model entry update for " + nodeMemo.getNodeID());
1310            if (propertyChangeEvent.getPropertyName().equals(UPDATE_PROP_SIMPLE_NODE_IDENT)) {
1311                updateDescription();
1312            }
1313        }
1314
1315        public void dispose() {
1316            //log.warning("dispose of " + nodeMemo.getNodeID().toString());
1317            nodeMemo.removePropertyChangeListener(this);
1318        }
1319    }
1320
1321    // --------------  load CDI data ---------
1322
1323    private void loadCdiData() {
1324        if (!replaceData()) {
1325            return;
1326        }
1327
1328        // Load data
1329        loadCdiInputs();
1330        loadCdiOutputs();
1331        loadCdiReceivers();
1332        loadCdiTransmitters();
1333        loadCdiGroups();
1334
1335        for (GroupRow row : _groupList) {
1336            decode(row);
1337        }
1338
1339        setDirty(false);
1340
1341        _groupTable.setRowSelectionInterval(0, 0);
1342
1343        _groupTable.repaint();
1344
1345        _exportButton.setEnabled(true);
1346        _refreshButton.setEnabled(true);
1347        _storeButton.setEnabled(true);
1348        _exportItem.setEnabled(true);
1349        _refreshItem.setEnabled(true);
1350        _storeItem.setEnabled(true);
1351    }
1352
1353    private void pushedRefreshButton(ActionEvent e) {
1354        loadCdiData();
1355    }
1356
1357    private void loadCdiGroups() {
1358        for (int i = 0; i < 16; i++) {
1359            var groupRow = _groupList.get(i);
1360            groupRow.clearLogicList();
1361
1362            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1363            groupRow.setName(entry.getValue());
1364            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1365            groupRow.setMultiLine(entry.getValue());
1366        }
1367
1368        _groupTable.revalidate();
1369    }
1370
1371    private void loadCdiInputs() {
1372        for (int i = 0; i < 16; i++) {
1373            for (int j = 0; j < 8; j++) {
1374                var inputRow = _inputList.get((i * 8) + j);
1375
1376                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1377                inputRow.setName(entry.getValue());
1378                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1379                inputRow.setEventTrue(event.getValue().toShortString());
1380                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1381                inputRow.setEventFalse(event.getValue().toShortString());
1382            }
1383        }
1384        _inputTable.revalidate();
1385    }
1386
1387    private void loadCdiOutputs() {
1388        for (int i = 0; i < 16; i++) {
1389            for (int j = 0; j < 8; j++) {
1390                var outputRow = _outputList.get((i * 8) + j);
1391
1392                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1393                outputRow.setName(entry.getValue());
1394                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1395                outputRow.setEventTrue(event.getValue().toShortString());
1396                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1397                outputRow.setEventFalse(event.getValue().toShortString());
1398            }
1399        }
1400        _outputTable.revalidate();
1401    }
1402
1403    private void loadCdiReceivers() {
1404        for (int i = 0; i < 16; i++) {
1405            var receiverRow = _receiverList.get(i);
1406
1407            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1408            receiverRow.setName(entry.getValue());
1409            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1410            receiverRow.setEventId(event.getValue().toShortString());
1411        }
1412        _receiverTable.revalidate();
1413    }
1414
1415    private void loadCdiTransmitters() {
1416        for (int i = 0; i < 16; i++) {
1417            var transmitterRow = _transmitterList.get(i);
1418
1419            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1420            transmitterRow.setName(entry.getValue());
1421            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_EVENT, i));
1422            transmitterRow.setEventId(event.getValue().toShortString());
1423        }
1424        _transmitterTable.revalidate();
1425    }
1426
1427    // --------------  store CDI data ---------
1428
1429    private void pushedStoreButton(ActionEvent e) {
1430        _csvMessages.clear();
1431        _compileNeeded = false;
1432        _storeQueueLength.set(0);
1433
1434        // Store CDI data
1435        storeInputs();
1436        storeOutputs();
1437        storeReceivers();
1438        storeTransmitters();
1439        storeGroups();
1440
1441        setDirty(false);
1442    }
1443
1444    private void storeGroups() {
1445        // store the group data
1446        int currentCount = 0;
1447
1448        for (int i = 0; i < 16; i++) {
1449            var row = _groupList.get(i);
1450
1451            // update the group line
1452            encode(row);
1453
1454            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1455            if (!row.getName().equals(entry.getValue())) {
1456                entry.addPropertyChangeListener(_entryListener);
1457                entry.setValue(row.getName());
1458                currentCount = _storeQueueLength.incrementAndGet();
1459            }
1460
1461            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1462            if (!row.getMultiLine().equals(entry.getValue())) {
1463                entry.addPropertyChangeListener(_entryListener);
1464                entry.setValue(row.getMultiLine());
1465                currentCount = _storeQueueLength.incrementAndGet();
1466                _compileNeeded = true;
1467            }
1468
1469            log.debug("Group: {}", row.getName());
1470            log.debug("Logic: {}", row.getMultiLine());
1471        }
1472        log.debug("storeGroups count = {}", currentCount);
1473    }
1474
1475    private void storeInputs() {
1476        int currentCount = 0;
1477
1478        for (int i = 0; i < 16; i++) {
1479            for (int j = 0; j < 8; j++) {
1480                var row = _inputList.get((i * 8) + j);
1481
1482                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1483                if (!row.getName().equals(entry.getValue())) {
1484                    entry.addPropertyChangeListener(_entryListener);
1485                    entry.setValue(row.getName());
1486                    currentCount = _storeQueueLength.incrementAndGet();
1487                }
1488
1489                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1490                if (!row.getEventTrue().equals(event.getValue().toShortString())) {
1491                    event.addPropertyChangeListener(_entryListener);
1492                    event.setValue(new EventID(row.getEventTrue()));
1493                    currentCount = _storeQueueLength.incrementAndGet();
1494                }
1495
1496                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1497                if (!row.getEventFalse().equals(event.getValue().toShortString())) {
1498                    event.addPropertyChangeListener(_entryListener);
1499                    event.setValue(new EventID(row.getEventFalse()));
1500                    currentCount = _storeQueueLength.incrementAndGet();
1501                }
1502            }
1503        }
1504        log.debug("storeInputs count = {}", currentCount);
1505    }
1506
1507    private void storeOutputs() {
1508        int currentCount = 0;
1509
1510        for (int i = 0; i < 16; i++) {
1511            for (int j = 0; j < 8; j++) {
1512                var row = _outputList.get((i * 8) + j);
1513
1514                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1515                if (!row.getName().equals(entry.getValue())) {
1516                    entry.addPropertyChangeListener(_entryListener);
1517                    entry.setValue(row.getName());
1518                    currentCount = _storeQueueLength.incrementAndGet();
1519                }
1520
1521                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1522                if (!row.getEventTrue().equals(event.getValue().toShortString())) {
1523                    event.addPropertyChangeListener(_entryListener);
1524                    event.setValue(new EventID(row.getEventTrue()));
1525                    currentCount = _storeQueueLength.incrementAndGet();
1526                }
1527
1528                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1529                if (!row.getEventFalse().equals(event.getValue().toShortString())) {
1530                    event.addPropertyChangeListener(_entryListener);
1531                    event.setValue(new EventID(row.getEventFalse()));
1532                    currentCount = _storeQueueLength.incrementAndGet();
1533                }
1534            }
1535        }
1536        log.debug("storeOutputs count = {}", currentCount);
1537    }
1538
1539    private void storeReceivers() {
1540        int currentCount = 0;
1541
1542        for (int i = 0; i < 16; i++) {
1543            var row = _receiverList.get(i);
1544
1545            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1546            if (!row.getName().equals(entry.getValue())) {
1547                entry.addPropertyChangeListener(_entryListener);
1548                entry.setValue(row.getName());
1549                currentCount = _storeQueueLength.incrementAndGet();
1550            }
1551
1552            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1553            if (!row.getEventId().equals(event.getValue().toShortString())) {
1554                event.addPropertyChangeListener(_entryListener);
1555                event.setValue(new EventID(row.getEventId()));
1556                currentCount = _storeQueueLength.incrementAndGet();
1557            }
1558        }
1559        log.debug("storeReceivers count = {}", currentCount);
1560    }
1561
1562    private void storeTransmitters() {
1563        int currentCount = 0;
1564
1565        for (int i = 0; i < 16; i++) {
1566            var row = _transmitterList.get(i);
1567
1568            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1569            if (!row.getName().equals(entry.getValue())) {
1570                entry.addPropertyChangeListener(_entryListener);
1571                entry.setValue(row.getName());
1572                currentCount = _storeQueueLength.incrementAndGet();
1573            }
1574        }
1575        log.debug("storeTransmitters count = {}", currentCount);
1576    }
1577
1578    // --------------  Backup Import ---------
1579
1580    private void loadBackupData(ActionEvent m) {
1581        if (!replaceData()) {
1582            return;
1583        }
1584
1585        var fileChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
1586        fileChooser.setApproveButtonText(Bundle.getMessage("LoadCdiButton"));
1587        fileChooser.setDialogTitle(Bundle.getMessage("LoadCdiTitle"));
1588        var filter = new FileNameExtensionFilter(Bundle.getMessage("LoadCdiFilter"), "txt");
1589        fileChooser.addChoosableFileFilter(filter);
1590        fileChooser.setFileFilter(filter);
1591
1592        int response = fileChooser.showOpenDialog(this);
1593        if (response == JFileChooser.CANCEL_OPTION) {
1594            return;
1595        }
1596
1597        List<String> lines = null;
1598        try {
1599            lines = Files.readAllLines(Paths.get(fileChooser.getSelectedFile().getAbsolutePath()));
1600        } catch (IOException e) {
1601            log.error("Failed to load file.", e);
1602            return;
1603        }
1604
1605        for (int i = 0; i < lines.size(); i++) {
1606            if (lines.get(i).startsWith("Logic Inputs.Group")) {
1607                loadBackupInputs(i, lines);
1608                i += 128 * 3;
1609            }
1610
1611            if (lines.get(i).startsWith("Logic Outputs.Group")) {
1612                loadBackupOutputs(i, lines);
1613                i += 128 * 3;
1614            }
1615            if (lines.get(i).startsWith("Track Receivers")) {
1616                loadBackupReceivers(i, lines);
1617                i += 16 * 2;
1618            }
1619            if (lines.get(i).startsWith("Track Transmitters")) {
1620                loadBackupTransmitters(i, lines);
1621                i += 16 * 2;
1622            }
1623            if (lines.get(i).startsWith("Conditionals.Logic")) {
1624                loadBackupGroups(i, lines);
1625                i += 16 * 2;
1626            }
1627        }
1628
1629        for (GroupRow row : _groupList) {
1630            decode(row);
1631        }
1632
1633        setDirty(false);
1634        _groupTable.setRowSelectionInterval(0, 0);
1635        _groupTable.repaint();
1636
1637        _exportButton.setEnabled(true);
1638        _exportItem.setEnabled(true);
1639    }
1640
1641    private String getLineValue(String line) {
1642        if (line.endsWith("=")) {
1643            return "";
1644        }
1645        int index = line.indexOf("=");
1646        var newLine = line.substring(index + 1);
1647        newLine = Util.unescapeString(newLine);
1648        return newLine;
1649    }
1650
1651    private void loadBackupInputs(int index, List<String> lines) {
1652        for (int i = 0; i < 128; i++) {
1653            var inputRow = _inputList.get(i);
1654
1655            inputRow.setName(getLineValue(lines.get(index)));
1656            inputRow.setEventTrue(getLineValue(lines.get(index + 1)));
1657            inputRow.setEventFalse(getLineValue(lines.get(index + 2)));
1658            index += 3;
1659        }
1660
1661        _inputTable.revalidate();
1662    }
1663
1664    private void loadBackupOutputs(int index, List<String> lines) {
1665        for (int i = 0; i < 128; i++) {
1666            var outputRow = _outputList.get(i);
1667
1668            outputRow.setName(getLineValue(lines.get(index)));
1669            outputRow.setEventTrue(getLineValue(lines.get(index + 1)));
1670            outputRow.setEventFalse(getLineValue(lines.get(index + 2)));
1671            index += 3;
1672        }
1673
1674        _outputTable.revalidate();
1675    }
1676
1677    private void loadBackupReceivers(int index, List<String> lines) {
1678        for (int i = 0; i < 16; i++) {
1679            var receiverRow = _receiverList.get(i);
1680
1681            receiverRow.setName(getLineValue(lines.get(index)));
1682            receiverRow.setEventId(getLineValue(lines.get(index + 1)));
1683            index += 2;
1684        }
1685
1686        _receiverTable.revalidate();
1687    }
1688
1689    private void loadBackupTransmitters(int index, List<String> lines) {
1690        for (int i = 0; i < 16; i++) {
1691            var transmitterRow = _transmitterList.get(i);
1692
1693            transmitterRow.setName(getLineValue(lines.get(index)));
1694            transmitterRow.setEventId(getLineValue(lines.get(index + 1)));
1695            index += 2;
1696        }
1697
1698        _transmitterTable.revalidate();
1699    }
1700
1701    private void loadBackupGroups(int index, List<String> lines) {
1702        for (int i = 0; i < 16; i++) {
1703            var groupRow = _groupList.get(i);
1704            groupRow.clearLogicList();
1705
1706            groupRow.setName(getLineValue(lines.get(index)));
1707            groupRow.setMultiLine(getLineValue(lines.get(index + 1)));
1708            index += 2;
1709        }
1710
1711        _groupTable.revalidate();
1712        _logicTable.revalidate();
1713    }
1714
1715    // --------------  CSV Import ---------
1716
1717    private void pushedImportButton(ActionEvent e) {
1718        if (!replaceData()) {
1719            return;
1720        }
1721
1722        if (!setCsvDirectoryPath(true)) {
1723            return;
1724        }
1725
1726        _csvMessages.clear();
1727        importCsvData();
1728        setDirty(false);
1729
1730        _exportButton.setEnabled(true);
1731        _exportItem.setEnabled(true);
1732
1733        if (!_csvMessages.isEmpty()) {
1734            JmriJOptionPane.showMessageDialog(this,
1735                    String.join("\n", _csvMessages),
1736                    Bundle.getMessage("TitleCsvImport"),
1737                    JmriJOptionPane.ERROR_MESSAGE);
1738        }
1739    }
1740
1741    private void importCsvData() {
1742        importGroupLogic();
1743        importInputs();
1744        importOutputs();
1745        importReceivers();
1746        importTransmitters();
1747
1748        _groupTable.setRowSelectionInterval(0, 0);
1749
1750        _groupTable.repaint();
1751    }
1752
1753    private void importGroupLogic() {
1754        List<CSVRecord> records = getCsvRecords("group_logic.csv");
1755        if (records.isEmpty()) {
1756            return;
1757        }
1758
1759        var skipHeader = true;
1760        int groupNumber = -1;
1761        for (CSVRecord record : records) {
1762            if (skipHeader) {
1763                skipHeader = false;
1764                continue;
1765            }
1766
1767            List<String> values = new ArrayList<>();
1768            record.forEach(values::add);
1769
1770            if (values.size() == 1) {
1771                // Create Group
1772                groupNumber++;
1773                var groupRow = _groupList.get(groupNumber);
1774                groupRow.setName(values.get(0));
1775                groupRow.setMultiLine("");
1776                groupRow.clearLogicList();
1777            } else if (values.size() == 5) {
1778                var oper = getEnum(values.get(2));
1779                var logicRow = new LogicRow(values.get(1), oper, values.get(3), values.get(4));
1780                _groupList.get(groupNumber).getLogicList().add(logicRow);
1781            } else {
1782                _csvMessages.add(Bundle.getMessage("ImportGroupError", record.toString()));
1783            }
1784        }
1785
1786        _groupTable.revalidate();
1787        _logicTable.revalidate();
1788    }
1789
1790    private void importInputs() {
1791        List<CSVRecord> records = getCsvRecords("inputs.csv");
1792        if (records.isEmpty()) {
1793            return;
1794        }
1795
1796        for (int i = 0; i < 129; i++) {
1797            if (i == 0) {
1798                continue;
1799            }
1800
1801            var record = records.get(i);
1802            List<String> values = new ArrayList<>();
1803            record.forEach(values::add);
1804
1805            if (values.size() == 4) {
1806                var inputRow = _inputList.get(i - 1);
1807                inputRow.setName(values.get(1));
1808                inputRow.setEventTrue(values.get(2));
1809                inputRow.setEventFalse(values.get(3));
1810            } else {
1811                _csvMessages.add(Bundle.getMessage("ImportInputError", record.toString()));
1812            }
1813        }
1814
1815        _inputTable.revalidate();
1816    }
1817
1818    private void importOutputs() {
1819        List<CSVRecord> records = getCsvRecords("outputs.csv");
1820        if (records.isEmpty()) {
1821            return;
1822        }
1823
1824        for (int i = 0; i < 129; i++) {
1825            if (i == 0) {
1826                continue;
1827            }
1828
1829            var record = records.get(i);
1830            List<String> values = new ArrayList<>();
1831            record.forEach(values::add);
1832
1833            if (values.size() == 4) {
1834                var outputRow = _outputList.get(i - 1);
1835                outputRow.setName(values.get(1));
1836                outputRow.setEventTrue(values.get(2));
1837                outputRow.setEventFalse(values.get(3));
1838            } else {
1839                _csvMessages.add(Bundle.getMessage("ImportOuputError", record.toString()));
1840            }
1841        }
1842
1843        _outputTable.revalidate();
1844    }
1845
1846    private void importReceivers() {
1847        List<CSVRecord> records = getCsvRecords("receivers.csv");
1848        if (records.isEmpty()) {
1849            return;
1850        }
1851
1852        for (int i = 0; i < 17; i++) {
1853            if (i == 0) {
1854                continue;
1855            }
1856
1857            var record = records.get(i);
1858            List<String> values = new ArrayList<>();
1859            record.forEach(values::add);
1860
1861            if (values.size() == 3) {
1862                var receiverRow = _receiverList.get(i - 1);
1863                receiverRow.setName(values.get(1));
1864                receiverRow.setEventId(values.get(2));
1865            } else {
1866                _csvMessages.add(Bundle.getMessage("ImportReceiverError", record.toString()));
1867            }
1868        }
1869
1870        _receiverTable.revalidate();
1871    }
1872
1873    private void importTransmitters() {
1874        List<CSVRecord> records = getCsvRecords("transmitters.csv");
1875        if (records.isEmpty()) {
1876            return;
1877        }
1878
1879        for (int i = 0; i < 17; i++) {
1880            if (i == 0) {
1881                continue;
1882            }
1883
1884            var record = records.get(i);
1885            List<String> values = new ArrayList<>();
1886            record.forEach(values::add);
1887
1888            if (values.size() == 3) {
1889                var transmitterRow = _transmitterList.get(i - 1);
1890                transmitterRow.setName(values.get(1));
1891                transmitterRow.setEventId(values.get(2));
1892            } else {
1893                _csvMessages.add(Bundle.getMessage("ImportTransmitterError", record.toString()));
1894            }
1895        }
1896
1897        _transmitterTable.revalidate();
1898    }
1899
1900    private List<CSVRecord> getCsvRecords(String fileName) {
1901        var recordList = new ArrayList<CSVRecord>();
1902        FileReader fileReader;
1903        try {
1904            fileReader = new FileReader(_csvDirectoryPath + fileName);
1905        } catch (FileNotFoundException ex) {
1906            _csvMessages.add(Bundle.getMessage("ImportFileNotFound", fileName));
1907            return recordList;
1908        }
1909
1910        BufferedReader bufferedReader;
1911        CSVParser csvFile;
1912
1913        try {
1914            bufferedReader = new BufferedReader(fileReader);
1915            csvFile = new CSVParser(bufferedReader, CSVFormat.DEFAULT);
1916            recordList.addAll(csvFile.getRecords());
1917            csvFile.close();
1918            bufferedReader.close();
1919            fileReader.close();
1920        } catch (IOException iox) {
1921            _csvMessages.add(Bundle.getMessage("ImportFileIOError", iox.getMessage(), fileName));
1922        }
1923
1924        return recordList;
1925    }
1926
1927    // --------------  CSV Export ---------
1928
1929    private void pushedExportButton(ActionEvent e) {
1930        if (!setCsvDirectoryPath(false)) {
1931            return;
1932        }
1933
1934        _csvMessages.clear();
1935        exportCsvData();
1936        setDirty(false);
1937
1938        _csvMessages.add(Bundle.getMessage("ExportDone"));
1939        var msgType = JmriJOptionPane.ERROR_MESSAGE;
1940        if (_csvMessages.size() == 1) {
1941            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
1942        }
1943        JmriJOptionPane.showMessageDialog(this,
1944                String.join("\n", _csvMessages),
1945                Bundle.getMessage("TitleCsvExport"),
1946                msgType);
1947    }
1948
1949    private void exportCsvData() {
1950        try {
1951            exportGroupLogic();
1952            exportInputs();
1953            exportOutputs();
1954            exportReceivers();
1955            exportTransmitters();
1956        } catch (IOException ex) {
1957            _csvMessages.add(Bundle.getMessage("ExportIOError", ex.getMessage()));
1958        }
1959
1960    }
1961
1962    private void exportGroupLogic() throws IOException {
1963        var fileWriter = new FileWriter(_csvDirectoryPath + "group_logic.csv");
1964        var bufferedWriter = new BufferedWriter(fileWriter);
1965        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1966
1967        csvFile.printRecord(Bundle.getMessage("GroupName"), Bundle.getMessage("ColumnLabel"),
1968                 Bundle.getMessage("ColumnOper"), Bundle.getMessage("ColumnName"), Bundle.getMessage("ColumnComment"));
1969
1970        for (int i = 0; i < 16; i++) {
1971            var row = _groupList.get(i);
1972            var groupName = row.getName();
1973            csvFile.printRecord(groupName);
1974            var logicRow = row.getLogicList();
1975            for (LogicRow logic : logicRow) {
1976                var operName = logic.getOperName();
1977                csvFile.printRecord("", logic.getLabel(), operName, logic.getName(), logic.getComment());
1978            }
1979        }
1980
1981        // Flush the write buffer and close the file
1982        csvFile.flush();
1983        csvFile.close();
1984    }
1985
1986    private void exportInputs() throws IOException {
1987        var fileWriter = new FileWriter(_csvDirectoryPath + "inputs.csv");
1988        var bufferedWriter = new BufferedWriter(fileWriter);
1989        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1990
1991        csvFile.printRecord(Bundle.getMessage("ColumnInput"), Bundle.getMessage("ColumnName"),
1992                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
1993
1994        for (int i = 0; i < 16; i++) {
1995            for (int j = 0; j < 8; j++) {
1996                var variable = "I" + i + "." + j;
1997                var row = _inputList.get((i * 8) + j);
1998                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
1999            }
2000        }
2001
2002        // Flush the write buffer and close the file
2003        csvFile.flush();
2004        csvFile.close();
2005    }
2006
2007    private void exportOutputs() throws IOException {
2008        var fileWriter = new FileWriter(_csvDirectoryPath + "outputs.csv");
2009        var bufferedWriter = new BufferedWriter(fileWriter);
2010        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2011
2012        csvFile.printRecord(Bundle.getMessage("ColumnOutput"), Bundle.getMessage("ColumnName"),
2013                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
2014
2015        for (int i = 0; i < 16; i++) {
2016            for (int j = 0; j < 8; j++) {
2017                var variable = "Q" + i + "." + j;
2018                var row = _outputList.get((i * 8) + j);
2019                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
2020            }
2021        }
2022
2023        // Flush the write buffer and close the file
2024        csvFile.flush();
2025        csvFile.close();
2026    }
2027
2028    private void exportReceivers() throws IOException {
2029        var fileWriter = new FileWriter(_csvDirectoryPath + "receivers.csv");
2030        var bufferedWriter = new BufferedWriter(fileWriter);
2031        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2032
2033        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2034                 Bundle.getMessage("ColumnEventID"));
2035
2036        for (int i = 0; i < 16; i++) {
2037            var variable = "Y" + i;
2038            var row = _receiverList.get(i);
2039            csvFile.printRecord(variable, row.getName(), row.getEventId());
2040        }
2041
2042        // Flush the write buffer and close the file
2043        csvFile.flush();
2044        csvFile.close();
2045    }
2046
2047    private void exportTransmitters() throws IOException {
2048        var fileWriter = new FileWriter(_csvDirectoryPath + "transmitters.csv");
2049        var bufferedWriter = new BufferedWriter(fileWriter);
2050        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2051
2052        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2053                 Bundle.getMessage("ColumnEventID"));
2054
2055        for (int i = 0; i < 16; i++) {
2056            var variable = "Z" + i;
2057            var row = _transmitterList.get(i);
2058            csvFile.printRecord(variable, row.getName(), row.getEventId());
2059        }
2060
2061        // Flush the write buffer and close the file
2062        csvFile.flush();
2063        csvFile.close();
2064    }
2065
2066    /**
2067     * Select the directory that will be used for the CSV file set.
2068     * @param isOpen - True for CSV Import and false for CSV Export.
2069     * @return true if a directory was selected.
2070     */
2071    private boolean setCsvDirectoryPath(boolean isOpen) {
2072        var directoryChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
2073        directoryChooser.setApproveButtonText(Bundle.getMessage("SelectCsvButton"));
2074        directoryChooser.setDialogTitle(Bundle.getMessage("SelectCsvTitle"));
2075        directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
2076
2077        int response = 0;
2078        if (isOpen) {
2079            response = directoryChooser.showOpenDialog(this);
2080        } else {
2081            response = directoryChooser.showSaveDialog(this);
2082        }
2083        if (response != JFileChooser.APPROVE_OPTION) {
2084            return false;
2085        }
2086        _csvDirectoryPath = directoryChooser.getSelectedFile().getAbsolutePath() + FileUtil.SEPARATOR;
2087
2088        return true;
2089    }
2090
2091    // --------------  Data Utilities ---------
2092
2093    private void setDirty(boolean dirty) {
2094        _dirty = dirty;
2095    }
2096
2097    private boolean isDirty() {
2098        return _dirty;
2099    }
2100
2101    private boolean replaceData() {
2102        if (isDirty()) {
2103            int response = JmriJOptionPane.showConfirmDialog(this,
2104                    Bundle.getMessage("MessageRevert"),
2105                    Bundle.getMessage("TitleRevert"),
2106                    JmriJOptionPane.YES_NO_OPTION);
2107            if (response != JmriJOptionPane.YES_OPTION) {
2108                return false;
2109            }
2110        }
2111        return true;
2112    }
2113
2114    private void warningDialog(String title, String message) {
2115        JmriJOptionPane.showMessageDialog(this,
2116            message,
2117            title,
2118            JmriJOptionPane.WARNING_MESSAGE);
2119    }
2120
2121    // --------------  Data validation ---------
2122
2123    static boolean isLabelValid(String label) {
2124        if (label.isEmpty()) {
2125            return true;
2126        }
2127
2128        var match = PARSE_LABEL.matcher(label);
2129        if (match.find()) {
2130            return true;
2131        }
2132
2133        JmriJOptionPane.showMessageDialog(null,
2134                Bundle.getMessage("MessageLabel", label),
2135                Bundle.getMessage("TitleLabel"),
2136                JmriJOptionPane.ERROR_MESSAGE);
2137        return false;
2138    }
2139
2140    static boolean isEventValid(String event) {
2141        var valid = true;
2142
2143        if (event.isEmpty()) {
2144            return valid;
2145        }
2146
2147        var hexPairs = event.split("\\.");
2148        if (hexPairs.length != 8) {
2149            valid = false;
2150        } else {
2151            for (int i = 0; i < 8; i++) {
2152                var match = PARSE_HEXPAIR.matcher(hexPairs[i]);
2153                if (!match.find()) {
2154                    valid = false;
2155                    break;
2156                }
2157            }
2158        }
2159
2160        if (!valid) {
2161            JmriJOptionPane.showMessageDialog(null,
2162                    Bundle.getMessage("MessageEvent", event),
2163                    Bundle.getMessage("TitleEvent"),
2164                    JmriJOptionPane.ERROR_MESSAGE);
2165            log.error("bad event: {}", event);
2166        }
2167
2168        return valid;
2169    }
2170
2171    // --------------  table lists ---------
2172
2173    /**
2174     * The Group row contains the name and the raw data for one of the 16 groups.
2175     * It also contains the decoded logic for the group in the logic list.
2176     */
2177    static class GroupRow {
2178        String _name;
2179        String _multiLine = "";
2180        List<LogicRow> _logicList = new ArrayList<>();
2181
2182
2183        GroupRow(String name) {
2184            _name = name;
2185        }
2186
2187        String getName() {
2188            return _name;
2189        }
2190
2191        void setName(String newName) {
2192            _name = newName;
2193        }
2194
2195        List<LogicRow> getLogicList() {
2196            return _logicList;
2197        }
2198
2199        void setLogicList(List<LogicRow> logicList) {
2200            _logicList.clear();
2201            _logicList.addAll(logicList);
2202        }
2203
2204        void clearLogicList() {
2205            _logicList.clear();
2206        }
2207
2208        String getMultiLine() {
2209            return _multiLine;
2210        }
2211
2212        void setMultiLine(String newMultiLine) {
2213            _multiLine = newMultiLine.strip();
2214        }
2215
2216        String getSize() {
2217            int size = (_multiLine.length() * 100) / 255;
2218            return String.valueOf(size) + "%";
2219        }
2220    }
2221
2222    /**
2223     * The definition of a logic row
2224     */
2225    static class LogicRow {
2226        String _label;
2227        Operator _oper;
2228        String _name;
2229        String _comment;
2230
2231        LogicRow(String label, Operator oper, String name, String comment) {
2232            _label = label;
2233            _oper = oper;
2234            _name = name;
2235            _comment = comment;
2236        }
2237
2238        String getLabel() {
2239            return _label;
2240        }
2241
2242        void setLabel(String newLabel) {
2243            var label = newLabel.trim();
2244            if (isLabelValid(label)) {
2245                _label = label;
2246            }
2247        }
2248
2249        Operator getOper() {
2250            return _oper;
2251        }
2252
2253        String getOperName() {
2254            if (_oper == null) {
2255                return "";
2256            }
2257
2258            String operName = _oper.name();
2259
2260            // Fix special enums
2261            if (operName.equals("Cp")) {
2262                operName = ")";
2263            } else if (operName.equals("EQ")) {
2264                operName = "=";
2265            } else if (operName.contains("p")) {
2266                operName = operName.replace("p", "(");
2267            }
2268
2269            return operName;
2270        }
2271
2272        void setOper(Operator newOper) {
2273            _oper = newOper;
2274        }
2275
2276        String getName() {
2277            return _name;
2278        }
2279
2280        void setName(String newName) {
2281            _name = newName.trim();
2282        }
2283
2284        String getComment() {
2285            return _comment;
2286        }
2287
2288        void setComment(String newComment) {
2289            _comment = newComment;
2290        }
2291    }
2292
2293    /**
2294     * The name and assigned true and false events for an Input.
2295     */
2296    static class InputRow {
2297        String _name;
2298        String _eventTrue;
2299        String _eventFalse;
2300
2301        InputRow(String name, String eventTrue, String eventFalse) {
2302            _name = name;
2303            _eventTrue = eventTrue;
2304            _eventFalse = eventFalse;
2305        }
2306
2307        String getName() {
2308            return _name;
2309        }
2310
2311        void setName(String newName) {
2312            _name = newName.trim();
2313        }
2314
2315        String getEventTrue() {
2316            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2317            return _eventTrue;
2318        }
2319
2320        void setEventTrue(String newEventTrue) {
2321            var event = newEventTrue.trim();
2322            if (isEventValid(event)) {
2323                _eventTrue = event;
2324            }
2325        }
2326
2327        String getEventFalse() {
2328            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2329            return _eventFalse;
2330        }
2331
2332        void setEventFalse(String newEventFalse) {
2333            var event = newEventFalse.trim();
2334            if (isEventValid(event)) {
2335                _eventFalse = event;
2336            }
2337        }
2338    }
2339
2340    /**
2341     * The name and assigned true and false events for an Output.
2342     */
2343    static class OutputRow {
2344        String _name;
2345        String _eventTrue;
2346        String _eventFalse;
2347
2348        OutputRow(String name, String eventTrue, String eventFalse) {
2349            _name = name;
2350            _eventTrue = eventTrue;
2351            _eventFalse = eventFalse;
2352        }
2353
2354        String getName() {
2355            return _name;
2356        }
2357
2358        void setName(String newName) {
2359            _name = newName.trim();
2360        }
2361
2362        String getEventTrue() {
2363            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2364            return _eventTrue;
2365        }
2366
2367        void setEventTrue(String newEventTrue) {
2368            var event = newEventTrue.trim();
2369            if (isEventValid(event)) {
2370                _eventTrue = event;
2371            }
2372        }
2373
2374        String getEventFalse() {
2375            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2376            return _eventFalse;
2377        }
2378
2379        void setEventFalse(String newEventFalse) {
2380            var event = newEventFalse.trim();
2381            if (isEventValid(event)) {
2382                _eventFalse = event;
2383            }
2384        }
2385    }
2386
2387    /**
2388     * The name and assigned event id for a circuit receiver.
2389     */
2390    static class ReceiverRow {
2391        String _name;
2392        String _eventid;
2393
2394        ReceiverRow(String name, String eventid) {
2395            _name = name;
2396            _eventid = eventid;
2397        }
2398
2399        String getName() {
2400            return _name;
2401        }
2402
2403        void setName(String newName) {
2404            _name = newName.trim();
2405        }
2406
2407        String getEventId() {
2408            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2409            return _eventid;
2410        }
2411
2412        void setEventId(String newEventid) {
2413            var event = newEventid.trim();
2414            if (isEventValid(event)) {
2415                _eventid = event;
2416            }
2417        }
2418    }
2419
2420    /**
2421     * The name and assigned event id for a circuit transmitter.
2422     */
2423    static class TransmitterRow {
2424        String _name;
2425        String _eventid;
2426
2427        TransmitterRow(String name, String eventid) {
2428            _name = name;
2429            _eventid = eventid;
2430        }
2431
2432        String getName() {
2433            return _name;
2434        }
2435
2436        void setName(String newName) {
2437            _name = newName.trim();
2438        }
2439
2440        String getEventId() {
2441            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2442            return _eventid;
2443        }
2444
2445        void setEventId(String newEventid) {
2446            var event = newEventid.trim();
2447            if (isEventValid(event)) {
2448                _eventid = event;
2449            }
2450        }
2451    }
2452
2453    // --------------  table models ---------
2454
2455    /**
2456     * TableModel for Group table entries.
2457     */
2458    class GroupModel extends AbstractTableModel {
2459
2460        GroupModel() {
2461        }
2462
2463        public static final int ROW_COLUMN = 0;
2464        public static final int NAME_COLUMN = 1;
2465
2466        @Override
2467        public int getRowCount() {
2468            return _groupList.size();
2469        }
2470
2471        @Override
2472        public int getColumnCount() {
2473            return 2;
2474        }
2475
2476        @Override
2477        public Class<?> getColumnClass(int c) {
2478            return String.class;
2479        }
2480
2481        @Override
2482        public String getColumnName(int col) {
2483            switch (col) {
2484                case ROW_COLUMN:
2485                    return "";
2486                case NAME_COLUMN:
2487                    return Bundle.getMessage("ColumnName");
2488                default:
2489                    return "unknown";  // NOI18N
2490            }
2491        }
2492
2493        @Override
2494        public Object getValueAt(int r, int c) {
2495            switch (c) {
2496                case ROW_COLUMN:
2497                    return r + 1;
2498                case NAME_COLUMN:
2499                    return _groupList.get(r).getName();
2500                default:
2501                    return null;
2502            }
2503        }
2504
2505        @Override
2506        public void setValueAt(Object type, int r, int c) {
2507            switch (c) {
2508                case NAME_COLUMN:
2509                    _groupList.get(r).setName((String) type);
2510                    setDirty(true);
2511                    break;
2512                default:
2513                    break;
2514            }
2515        }
2516
2517        @Override
2518        public boolean isCellEditable(int r, int c) {
2519            return (c == NAME_COLUMN);
2520        }
2521
2522        public int getPreferredWidth(int col) {
2523            switch (col) {
2524                case ROW_COLUMN:
2525                    return new JTextField(4).getPreferredSize().width;
2526                case NAME_COLUMN:
2527                    return new JTextField(20).getPreferredSize().width;
2528                default:
2529                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2530                    return new JTextField(8).getPreferredSize().width;
2531            }
2532        }
2533    }
2534
2535    /**
2536     * TableModel for STL table entries.
2537     */
2538    class LogicModel extends AbstractTableModel {
2539
2540        LogicModel() {
2541        }
2542
2543        public static final int LABEL_COLUMN = 0;
2544        public static final int OPER_COLUMN = 1;
2545        public static final int NAME_COLUMN = 2;
2546        public static final int COMMENT_COLUMN = 3;
2547
2548        @Override
2549        public int getRowCount() {
2550            var logicList = _groupList.get(_groupRow).getLogicList();
2551            return logicList.size();
2552        }
2553
2554        @Override
2555        public int getColumnCount() {
2556            return 4;
2557        }
2558
2559        @Override
2560        public Class<?> getColumnClass(int c) {
2561            if (c == OPER_COLUMN) return JComboBox.class;
2562            return String.class;
2563        }
2564
2565        @Override
2566        public String getColumnName(int col) {
2567            switch (col) {
2568                case LABEL_COLUMN:
2569                    return Bundle.getMessage("ColumnLabel");  // NOI18N
2570                case OPER_COLUMN:
2571                    return Bundle.getMessage("ColumnOper");  // NOI18N
2572                case NAME_COLUMN:
2573                    return Bundle.getMessage("ColumnName");  // NOI18N
2574                case COMMENT_COLUMN:
2575                    return Bundle.getMessage("ColumnComment");  // NOI18N
2576                default:
2577                    return "unknown";  // NOI18N
2578            }
2579        }
2580
2581        @Override
2582        public Object getValueAt(int r, int c) {
2583            var logicList = _groupList.get(_groupRow).getLogicList();
2584            switch (c) {
2585                case LABEL_COLUMN:
2586                    return logicList.get(r).getLabel();
2587                case OPER_COLUMN:
2588                    return logicList.get(r).getOper();
2589                case NAME_COLUMN:
2590                    return logicList.get(r).getName();
2591                case COMMENT_COLUMN:
2592                    return logicList.get(r).getComment();
2593                default:
2594                    return null;
2595            }
2596        }
2597
2598        @Override
2599        public void setValueAt(Object type, int r, int c) {
2600            var logicList = _groupList.get(_groupRow).getLogicList();
2601            switch (c) {
2602                case LABEL_COLUMN:
2603                    logicList.get(r).setLabel((String) type);
2604                    setDirty(true);
2605                    break;
2606                case OPER_COLUMN:
2607                    var z = (Operator) type;
2608                    if (z != null) {
2609                        if (z.name().startsWith("z")) {
2610                            return;
2611                        }
2612                        if (z.name().equals("x0")) {
2613                            logicList.get(r).setOper(null);
2614                            return;
2615                        }
2616                    }
2617                    logicList.get(r).setOper((Operator) type);
2618                    setDirty(true);
2619                    break;
2620                case NAME_COLUMN:
2621                    logicList.get(r).setName((String) type);
2622                    setDirty(true);
2623                    break;
2624                case COMMENT_COLUMN:
2625                    logicList.get(r).setComment((String) type);
2626                    setDirty(true);
2627                    break;
2628                default:
2629                    break;
2630            }
2631        }
2632
2633        @Override
2634        public boolean isCellEditable(int r, int c) {
2635            return true;
2636        }
2637
2638        public int getPreferredWidth(int col) {
2639            switch (col) {
2640                case LABEL_COLUMN:
2641                    return new JTextField(6).getPreferredSize().width;
2642                case OPER_COLUMN:
2643                    return new JTextField(20).getPreferredSize().width;
2644                case NAME_COLUMN:
2645                case COMMENT_COLUMN:
2646                    return new JTextField(40).getPreferredSize().width;
2647                default:
2648                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2649                    return new JTextField(8).getPreferredSize().width;
2650            }
2651        }
2652    }
2653
2654    /**
2655     * TableModel for Input table entries.
2656     */
2657    class InputModel extends AbstractTableModel {
2658
2659        InputModel() {
2660        }
2661
2662        public static final int INPUT_COLUMN = 0;
2663        public static final int NAME_COLUMN = 1;
2664        public static final int TRUE_COLUMN = 2;
2665        public static final int FALSE_COLUMN = 3;
2666
2667        @Override
2668        public int getRowCount() {
2669            return _inputList.size();
2670        }
2671
2672        @Override
2673        public int getColumnCount() {
2674            return 4;
2675        }
2676
2677        @Override
2678        public Class<?> getColumnClass(int c) {
2679            return String.class;
2680        }
2681
2682        @Override
2683        public String getColumnName(int col) {
2684            switch (col) {
2685                case INPUT_COLUMN:
2686                    return Bundle.getMessage("ColumnInput");  // NOI18N
2687                case NAME_COLUMN:
2688                    return Bundle.getMessage("ColumnName");  // NOI18N
2689                case TRUE_COLUMN:
2690                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2691                case FALSE_COLUMN:
2692                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2693                default:
2694                    return "unknown";  // NOI18N
2695            }
2696        }
2697
2698        @Override
2699        public Object getValueAt(int r, int c) {
2700            switch (c) {
2701                case INPUT_COLUMN:
2702                    int grp = r / 8;
2703                    int rem = r % 8;
2704                    return "I" + grp + "." + rem;
2705                case NAME_COLUMN:
2706                    return _inputList.get(r).getName();
2707                case TRUE_COLUMN:
2708                    return _inputList.get(r).getEventTrue();
2709                case FALSE_COLUMN:
2710                    return _inputList.get(r).getEventFalse();
2711                default:
2712                    return null;
2713            }
2714        }
2715
2716        @Override
2717        public void setValueAt(Object type, int r, int c) {
2718            switch (c) {
2719                case NAME_COLUMN:
2720                    _inputList.get(r).setName((String) type);
2721                    setDirty(true);
2722                    break;
2723                case TRUE_COLUMN:
2724                    _inputList.get(r).setEventTrue((String) type);
2725                    setDirty(true);
2726                    break;
2727                case FALSE_COLUMN:
2728                    _inputList.get(r).setEventFalse((String) type);
2729                    setDirty(true);
2730                    break;
2731                default:
2732                    break;
2733            }
2734        }
2735
2736        @Override
2737        public boolean isCellEditable(int r, int c) {
2738            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2739        }
2740
2741        public int getPreferredWidth(int col) {
2742            switch (col) {
2743                case INPUT_COLUMN:
2744                    return new JTextField(6).getPreferredSize().width;
2745                case NAME_COLUMN:
2746                    return new JTextField(50).getPreferredSize().width;
2747                case TRUE_COLUMN:
2748                case FALSE_COLUMN:
2749                    return new JTextField(20).getPreferredSize().width;
2750                default:
2751                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2752                    return new JTextField(8).getPreferredSize().width;
2753            }
2754        }
2755    }
2756
2757    /**
2758     * TableModel for Output table entries.
2759     */
2760    class OutputModel extends AbstractTableModel {
2761        OutputModel() {
2762        }
2763
2764        public static final int OUTPUT_COLUMN = 0;
2765        public static final int NAME_COLUMN = 1;
2766        public static final int TRUE_COLUMN = 2;
2767        public static final int FALSE_COLUMN = 3;
2768
2769        @Override
2770        public int getRowCount() {
2771            return _outputList.size();
2772        }
2773
2774        @Override
2775        public int getColumnCount() {
2776            return 4;
2777        }
2778
2779        @Override
2780        public Class<?> getColumnClass(int c) {
2781            return String.class;
2782        }
2783
2784        @Override
2785        public String getColumnName(int col) {
2786            switch (col) {
2787                case OUTPUT_COLUMN:
2788                    return Bundle.getMessage("ColumnOutput");  // NOI18N
2789                case NAME_COLUMN:
2790                    return Bundle.getMessage("ColumnName");  // NOI18N
2791                case TRUE_COLUMN:
2792                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2793                case FALSE_COLUMN:
2794                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2795                default:
2796                    return "unknown";  // NOI18N
2797            }
2798        }
2799
2800        @Override
2801        public Object getValueAt(int r, int c) {
2802            switch (c) {
2803                case OUTPUT_COLUMN:
2804                    int grp = r / 8;
2805                    int rem = r % 8;
2806                    return "Q" + grp + "." + rem;
2807                case NAME_COLUMN:
2808                    return _outputList.get(r).getName();
2809                case TRUE_COLUMN:
2810                    return _outputList.get(r).getEventTrue();
2811                case FALSE_COLUMN:
2812                    return _outputList.get(r).getEventFalse();
2813                default:
2814                    return null;
2815            }
2816        }
2817
2818        @Override
2819        public void setValueAt(Object type, int r, int c) {
2820            switch (c) {
2821                case NAME_COLUMN:
2822                    _outputList.get(r).setName((String) type);
2823                    setDirty(true);
2824                    break;
2825                case TRUE_COLUMN:
2826                    _outputList.get(r).setEventTrue((String) type);
2827                    setDirty(true);
2828                    break;
2829                case FALSE_COLUMN:
2830                    _outputList.get(r).setEventFalse((String) type);
2831                    setDirty(true);
2832                    break;
2833                default:
2834                    break;
2835            }
2836        }
2837
2838        @Override
2839        public boolean isCellEditable(int r, int c) {
2840            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2841        }
2842
2843        public int getPreferredWidth(int col) {
2844            switch (col) {
2845                case OUTPUT_COLUMN:
2846                    return new JTextField(6).getPreferredSize().width;
2847                case NAME_COLUMN:
2848                    return new JTextField(50).getPreferredSize().width;
2849                case TRUE_COLUMN:
2850                case FALSE_COLUMN:
2851                    return new JTextField(20).getPreferredSize().width;
2852                default:
2853                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2854                    return new JTextField(8).getPreferredSize().width;
2855            }
2856        }
2857    }
2858
2859    /**
2860     * TableModel for circuit receiver table entries.
2861     */
2862    class ReceiverModel extends AbstractTableModel {
2863
2864        ReceiverModel() {
2865        }
2866
2867        public static final int CIRCUIT_COLUMN = 0;
2868        public static final int NAME_COLUMN = 1;
2869        public static final int EVENTID_COLUMN = 2;
2870
2871        @Override
2872        public int getRowCount() {
2873            return _receiverList.size();
2874        }
2875
2876        @Override
2877        public int getColumnCount() {
2878            return 3;
2879        }
2880
2881        @Override
2882        public Class<?> getColumnClass(int c) {
2883            return String.class;
2884        }
2885
2886        @Override
2887        public String getColumnName(int col) {
2888            switch (col) {
2889                case CIRCUIT_COLUMN:
2890                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
2891                case NAME_COLUMN:
2892                    return Bundle.getMessage("ColumnName");  // NOI18N
2893                case EVENTID_COLUMN:
2894                    return Bundle.getMessage("ColumnEventID");  // NOI18N
2895                default:
2896                    return "unknown";  // NOI18N
2897            }
2898        }
2899
2900        @Override
2901        public Object getValueAt(int r, int c) {
2902            switch (c) {
2903                case CIRCUIT_COLUMN:
2904                    return "Y" + r;
2905                case NAME_COLUMN:
2906                    return _receiverList.get(r).getName();
2907                case EVENTID_COLUMN:
2908                    return _receiverList.get(r).getEventId();
2909                default:
2910                    return null;
2911            }
2912        }
2913
2914        @Override
2915        public void setValueAt(Object type, int r, int c) {
2916            switch (c) {
2917                case NAME_COLUMN:
2918                    _receiverList.get(r).setName((String) type);
2919                    setDirty(true);
2920                    break;
2921                case EVENTID_COLUMN:
2922                    _receiverList.get(r).setEventId((String) type);
2923                    setDirty(true);
2924                    break;
2925                default:
2926                    break;
2927            }
2928        }
2929
2930        @Override
2931        public boolean isCellEditable(int r, int c) {
2932            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
2933        }
2934
2935        public int getPreferredWidth(int col) {
2936            switch (col) {
2937                case CIRCUIT_COLUMN:
2938                    return new JTextField(6).getPreferredSize().width;
2939                case NAME_COLUMN:
2940                    return new JTextField(50).getPreferredSize().width;
2941                case EVENTID_COLUMN:
2942                    return new JTextField(20).getPreferredSize().width;
2943                default:
2944                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2945                    return new JTextField(8).getPreferredSize().width;
2946            }
2947        }
2948    }
2949
2950    /**
2951     * TableModel for circuit transmitter table entries.
2952     */
2953    class TransmitterModel extends AbstractTableModel {
2954
2955        TransmitterModel() {
2956        }
2957
2958        public static final int CIRCUIT_COLUMN = 0;
2959        public static final int NAME_COLUMN = 1;
2960        public static final int EVENTID_COLUMN = 2;
2961
2962        @Override
2963        public int getRowCount() {
2964            return _transmitterList.size();
2965        }
2966
2967        @Override
2968        public int getColumnCount() {
2969            return 3;
2970        }
2971
2972        @Override
2973        public Class<?> getColumnClass(int c) {
2974            return String.class;
2975        }
2976
2977        @Override
2978        public String getColumnName(int col) {
2979            switch (col) {
2980                case CIRCUIT_COLUMN:
2981                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
2982                case NAME_COLUMN:
2983                    return Bundle.getMessage("ColumnName");  // NOI18N
2984                case EVENTID_COLUMN:
2985                    return Bundle.getMessage("ColumnEventID");  // NOI18N
2986                default:
2987                    return "unknown";  // NOI18N
2988            }
2989        }
2990
2991        @Override
2992        public Object getValueAt(int r, int c) {
2993            switch (c) {
2994                case CIRCUIT_COLUMN:
2995                    return "Z" + r;
2996                case NAME_COLUMN:
2997                    return _transmitterList.get(r).getName();
2998                case EVENTID_COLUMN:
2999                    return _transmitterList.get(r).getEventId();
3000                default:
3001                    return null;
3002            }
3003        }
3004
3005        @Override
3006        public void setValueAt(Object type, int r, int c) {
3007            switch (c) {
3008                case NAME_COLUMN:
3009                    _transmitterList.get(r).setName((String) type);
3010                    setDirty(true);
3011                    break;
3012                case EVENTID_COLUMN:
3013                    _transmitterList.get(r).setEventId((String) type);
3014                    setDirty(true);
3015                    break;
3016                default:
3017                    break;
3018            }
3019        }
3020
3021        @Override
3022        public boolean isCellEditable(int r, int c) {
3023            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
3024        }
3025
3026        public int getPreferredWidth(int col) {
3027            switch (col) {
3028                case CIRCUIT_COLUMN:
3029                    return new JTextField(6).getPreferredSize().width;
3030                case NAME_COLUMN:
3031                    return new JTextField(50).getPreferredSize().width;
3032                case EVENTID_COLUMN:
3033                    return new JTextField(20).getPreferredSize().width;
3034                default:
3035                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3036                    return new JTextField(8).getPreferredSize().width;
3037            }
3038        }
3039    }
3040
3041    // --------------  Operator Enum ---------
3042
3043    public enum Operator {
3044        x0(Bundle.getMessage("Separator0")),
3045        z1(Bundle.getMessage("Separator1")),
3046        A(Bundle.getMessage("OperatorA")),
3047        AN(Bundle.getMessage("OperatorAN")),
3048        O(Bundle.getMessage("OperatorO")),
3049        ON(Bundle.getMessage("OperatorON")),
3050        X(Bundle.getMessage("OperatorX")),
3051        XN(Bundle.getMessage("OperatorXN")),
3052
3053        z2(Bundle.getMessage("Separator2")),    // The STL parens are represented by lower case p
3054        Ap(Bundle.getMessage("OperatorAp")),
3055        ANp(Bundle.getMessage("OperatorANp")),
3056        Op(Bundle.getMessage("OperatorOp")),
3057        ONp(Bundle.getMessage("OperatorONp")),
3058        Xp(Bundle.getMessage("OperatorXp")),
3059        XNp(Bundle.getMessage("OperatorXNp")),
3060        Cp(Bundle.getMessage("OperatorCp")),    // Close paren
3061
3062        z3(Bundle.getMessage("Separator3")),
3063        EQ(Bundle.getMessage("OperatorEQ")),    // = operator
3064        R(Bundle.getMessage("OperatorR")),
3065        S(Bundle.getMessage("OperatorS")),
3066
3067        z4(Bundle.getMessage("Separator4")),
3068        NOT(Bundle.getMessage("OperatorNOT")),
3069        SET(Bundle.getMessage("OperatorSET")),
3070        CLR(Bundle.getMessage("OperatorCLR")),
3071        SAVE(Bundle.getMessage("OperatorSAVE")),
3072
3073        z5(Bundle.getMessage("Separator5")),
3074        JU(Bundle.getMessage("OperatorJU")),
3075        JC(Bundle.getMessage("OperatorJC")),
3076        JCN(Bundle.getMessage("OperatorJCN")),
3077        JCB(Bundle.getMessage("OperatorJCB")),
3078        JNB(Bundle.getMessage("OperatorJNB")),
3079        JBI(Bundle.getMessage("OperatorJBI")),
3080        JNBI(Bundle.getMessage("OperatorJNBI")),
3081
3082        z6(Bundle.getMessage("Separator6")),
3083        FN(Bundle.getMessage("OperatorFN")),
3084        FP(Bundle.getMessage("OperatorFP")),
3085
3086        z7(Bundle.getMessage("Separator7")),
3087        L(Bundle.getMessage("OperatorL")),
3088        FR(Bundle.getMessage("OperatorFR")),
3089        SP(Bundle.getMessage("OperatorSP")),
3090        SE(Bundle.getMessage("OperatorSE")),
3091        SD(Bundle.getMessage("OperatorSD")),
3092        SS(Bundle.getMessage("OperatorSS")),
3093        SF(Bundle.getMessage("OperatorSF"));
3094
3095        private final String _text;
3096
3097        private Operator(String text) {
3098            this._text = text;
3099        }
3100
3101        @Override
3102        public String toString() {
3103            return _text;
3104        }
3105
3106    }
3107
3108    // --------------  Token Class ---------
3109
3110    static class Token {
3111        String _type = "";
3112        String _name = "";
3113        int _offsetStart = 0;
3114        int _offsetEnd = 0;
3115
3116        Token(String type, String name, int offsetStart, int offsetEnd) {
3117            _type = type;
3118            _name = name;
3119            _offsetStart = offsetStart;
3120            _offsetEnd = offsetEnd;
3121        }
3122
3123        public String getType() {
3124            return _type;
3125        }
3126
3127        public String getName() {
3128            return _name;
3129        }
3130
3131        public int getStart() {
3132            return _offsetStart;
3133        }
3134
3135        public int getEnd() {
3136            return _offsetEnd;
3137        }
3138
3139        @Override
3140        public String toString() {
3141            return String.format("Type: %s, Name: %s, Start: %d, End: %d",
3142                    _type, _name, _offsetStart, _offsetEnd);
3143        }
3144    }
3145
3146    // --------------  misc items ---------
3147    @Override
3148    public java.util.List<JMenu> getMenus() {
3149        // create a file menu
3150        var retval = new ArrayList<JMenu>();
3151        var fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
3152
3153        _refreshItem = new JMenuItem(Bundle.getMessage("MenuRefresh"));
3154        _storeItem = new JMenuItem(Bundle.getMessage("MenuStore"));
3155        _importItem = new JMenuItem(Bundle.getMessage("MenuImport"));
3156        _exportItem = new JMenuItem(Bundle.getMessage("MenuExport"));
3157        _loadItem = new JMenuItem(Bundle.getMessage("MenuLoad"));
3158
3159        _refreshItem.addActionListener(this::pushedRefreshButton);
3160        _storeItem.addActionListener(this::pushedStoreButton);
3161        _importItem.addActionListener(this::pushedImportButton);
3162        _exportItem.addActionListener(this::pushedExportButton);
3163        _loadItem.addActionListener(this::loadBackupData);
3164
3165        fileMenu.add(_refreshItem);
3166        fileMenu.add(_storeItem);
3167        fileMenu.addSeparator();
3168        fileMenu.add(_importItem);
3169        fileMenu.add(_exportItem);
3170        fileMenu.addSeparator();
3171        fileMenu.add(_loadItem);
3172
3173        _refreshItem.setEnabled(false);
3174        _storeItem.setEnabled(false);
3175        _exportItem.setEnabled(false);
3176
3177        retval.add(fileMenu);
3178        return retval;
3179    }
3180
3181    @Override
3182    public void dispose() {
3183        _pm.setSimplePreferenceState(_storeModeCheck, _compactOption.isSelected());
3184        // and complete this
3185        super.dispose();
3186    }
3187
3188    @Override
3189    public String getHelpTarget() {
3190        return "package.jmri.jmrix.openlcb.swing.stleditor.StlEditorPane";
3191    }
3192
3193    @Override
3194    public String getTitle() {
3195        if (_canMemo != null) {
3196            return (_canMemo.getUserName() + " STL Editor");
3197        }
3198        return Bundle.getMessage("TitleSTLEditor");
3199    }
3200
3201    /**
3202     * Nested class to create one of these using old-style defaults
3203     */
3204    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
3205
3206        public Default() {
3207            super("STL Editor",
3208                    new jmri.util.swing.sdi.JmriJFrameInterface(),
3209                    StlEditorPane.class.getName(),
3210                    jmri.InstanceManager.getDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3211        }
3212    }
3213
3214    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StlEditorPane.class);
3215}