001package jmri.jmrix.openlcb.swing.eventtable;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.beans.*;
006import java.io.*;
007import java.util.*;
008
009import javax.swing.*;
010import javax.swing.table.*;
011
012import jmri.*;
013import jmri.jmrix.can.CanSystemConnectionMemo;
014import jmri.jmrix.openlcb.*;
015import jmri.util.ThreadingUtil;
016
017import jmri.swing.JmriJTablePersistenceManager;
018import jmri.util.swing.MultiLineCellRenderer;
019import jmri.util.table.ButtonEditor;
020import jmri.util.table.ButtonRenderer;
021
022import org.openlcb.*;
023import org.openlcb.implementations.*;
024import org.openlcb.swing.*;
025
026
027/**
028 * Pane for displaying a table of relationships of nodes, producers and consumers.
029 *
030 * @author Bob Jacobsen Copyright (C) 2023, 2026
031 * @since 5.3.4
032 */
033public class EventTablePane extends jmri.util.swing.JmriPanel
034        implements jmri.jmrix.can.swing.CanPanelInterface {
035
036    protected CanSystemConnectionMemo memo;
037    Connection connection;
038    NodeID nid;
039    OlcbEventNameStore nameStore;
040    OlcbNodeGroupStore groupStore;
041
042    MimicNodeStore mimcStore;
043    EventTableDataModel model;
044    JTable table;
045    Monitor monitor;
046
047    JComboBox<String> matchGroupName;   // required group name to display; index <= 0 is all
048    JCheckBox showRequiresLabel; // requires a user-provided name to display
049    JCheckBox showRequiresMatch; // requires at least one consumer and one producer exist to display
050    JCheckBox popcorn;           // popcorn mode displays events in real time
051
052    JFormattedTextField findID;
053    JTextField findTextID;
054
055    private transient TableRowSorter<EventTableDataModel> sorter;
056
057    public String getTitle(String menuTitle) {
058        return Bundle.getMessage("TitleEventTable");
059    }
060
061    @Override
062    public void initComponents(CanSystemConnectionMemo memo) {
063        this.memo = memo;
064        this.connection = memo.get(Connection.class);
065        this.nid = memo.get(NodeID.class);
066        this.nameStore = memo.get(OlcbEventNameStore.class);
067        this.groupStore = InstanceManager.getDefault(OlcbNodeGroupStore.class);
068        this.mimcStore = memo.get(MimicNodeStore.class);
069        EventTable stdEventTable = memo.get(OlcbInterface.class).getEventTable();
070
071        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
072
073        model = new EventTableDataModel(mimcStore, stdEventTable, nameStore);
074        sorter = new TableRowSorter<>(model);
075
076
077        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
078
079        // Add to GUI here
080
081        table = new JTable(model);
082
083        model.table = table;
084        model.sorter = sorter;
085        table.setAutoCreateRowSorter(true);
086        table.setRowSorter(sorter);
087        table.setDefaultRenderer(String.class, new MultiLineCellRenderer());
088        table.setShowGrid(true);
089        table.setGridColor(Color.BLACK);
090        table.getTableHeader().setBackground(Color.LIGHT_GRAY);
091        table.setName("jmri.jmrix.openlcb.swing.eventtable.EventTablePane.table"); // for persistence
092        table.setColumnSelectionAllowed(true);
093        table.setRowSelectionAllowed(true);
094        ButtonRenderer buttonRenderer = new ButtonRenderer();
095        table.setDefaultRenderer(JButton.class, buttonRenderer);
096        TableCellEditor buttonEditor = new ButtonEditor(new JButton());
097        table.setDefaultEditor(JButton.class, buttonEditor);
098
099        
100        // render in fixed size font
101        var defaultFont = table.getFont();
102        var fixedFont = new Font(Font.MONOSPACED, Font.PLAIN, defaultFont.getSize());
103        table.setFont(fixedFont);
104
105        var scrollPane = new JScrollPane(table);
106
107        // restore the column layout and start monitoring it
108        InstanceManager.getOptionalDefault(JmriJTablePersistenceManager.class).ifPresent((tpm) -> {
109            tpm.resetState(table);
110            tpm.persist(table);
111        });
112
113        add(scrollPane);
114
115        var buttonPanel = new JToolBar();
116        buttonPanel.setLayout(new jmri.util.swing.WrapLayout());
117
118        add(buttonPanel);
119
120        var updateButton = new JButton(Bundle.getMessage("ButtonUpdate"));
121        updateButton.addActionListener(this::sendRequestEvents); 
122        updateButton.setToolTipText(Bundle.getMessage("ButtonUpdateTt"));
123        buttonPanel.add(updateButton);
124        
125        matchGroupName = new JComboBox<>();
126        updateMatchGroupName();     // before adding listener
127        matchGroupName.addActionListener((ActionEvent e) -> {
128            filter();
129        });
130        groupStore.addPropertyChangeListener((PropertyChangeEvent evt) -> {
131            updateMatchGroupName();
132        });
133        buttonPanel.add(matchGroupName);
134        
135        showRequiresLabel = new JCheckBox(Bundle.getMessage("BoxShowRequiresLabel"));
136        showRequiresLabel.addActionListener((ActionEvent e) -> {
137            filter();
138        });
139        showRequiresLabel.setToolTipText(Bundle.getMessage("BoxShowRequiresLabelTt"));
140        showRequiresLabel.setOpaque(false); // make transparent
141        buttonPanel.add(showRequiresLabel);
142
143        showRequiresMatch = new JCheckBox(Bundle.getMessage("BoxShowRequiresMatch"));
144        showRequiresMatch.addActionListener((ActionEvent e) -> {
145            filter();
146        });
147        showRequiresMatch.setToolTipText(Bundle.getMessage("BoxShowRequiresMatchTt"));
148        showRequiresMatch.setOpaque(false); // make transparent
149        buttonPanel.add(showRequiresMatch);
150
151        popcorn = new JCheckBox(Bundle.getMessage("BoxPopcorn"));
152        popcorn.addActionListener((ActionEvent e) -> {
153            popcornButtonChanged();
154        });
155        popcorn.setToolTipText(Bundle.getMessage("BoxPopcornTt"));
156        popcorn.setOpaque(false); // make transparent
157        buttonPanel.add(popcorn);
158
159        JPanel findpanel = new JPanel(); // keep button and text together
160        findpanel.setOpaque(false); // make transparent
161        findpanel.setToolTipText(Bundle.getMessage("FindPanelFindEventTt"));
162        buttonPanel.add(findpanel);
163        
164        JLabel find = new JLabel(Bundle.getMessage("FindPanelFindEvent"));
165        findpanel.add(find);
166
167        findID = new EventIdTextField();
168        findID.setToolTipText(Bundle.getMessage("FindPanelFindEventFieldTt"));
169        findID.addActionListener(this::findRequested);
170        findID.addKeyListener(new KeyListener() {
171            @Override
172            public void keyTyped(KeyEvent keyEvent) {
173           }
174
175            @Override
176            public void keyReleased(KeyEvent keyEvent) {
177                // on release so the searchField has been updated
178                log.trace("keyTyped {} content {}", keyEvent.getKeyCode(), findTextID.getText());
179                findRequested(null);
180            }
181
182            @Override
183            public void keyPressed(KeyEvent keyEvent) {
184            }
185        });
186        findpanel.add(findID);
187        JButton addButton = new JButton(Bundle.getMessage("FindPanelButtonAdd"));
188        addButton.addActionListener(this::addRequested);
189        addButton.setToolTipText(Bundle.getMessage("FindPanelButtonAddTt"));        
190        findpanel.add(addButton);
191
192        findpanel = new JPanel();  // keep button and text together
193        findpanel.setOpaque(false); // make transparent
194        findpanel.setToolTipText(Bundle.getMessage("FindPanelFindNameTt"));
195        buttonPanel.add(findpanel);
196
197        JLabel findText = new JLabel(Bundle.getMessage("FindPanelFindName"));
198        findpanel.add(findText);
199
200        findTextID = new JTextField(16);
201        findTextID.addActionListener(this::findTextRequested);
202        findTextID.setToolTipText(Bundle.getMessage("FindPanelFindNameTt"));
203        findTextID.addKeyListener(new KeyListener() {
204            @Override
205            public void keyTyped(KeyEvent keyEvent) {
206           }
207
208            @Override
209            public void keyReleased(KeyEvent keyEvent) {
210                // on release so the searchField has been updated
211                log.trace("keyTyped {} content {}", keyEvent.getKeyCode(), findTextID.getText());
212                findTextRequested(null);
213            }
214
215            @Override
216            public void keyPressed(KeyEvent keyEvent) {
217            }
218        });
219        findpanel.add(findTextID);        
220
221        JButton sensorButton = new JButton(Bundle.getMessage("FindPanelButtonSensor"));
222        sensorButton.addActionListener(this::sensorRequested);
223        sensorButton.setToolTipText(Bundle.getMessage("FindPanelButtonSensorTt"));
224        buttonPanel.add(sensorButton);
225        
226        JButton turnoutButton = new JButton(Bundle.getMessage("FindPanelButtonTurnout"));
227        turnoutButton.addActionListener(this::turnoutRequested);
228        turnoutButton.setToolTipText(Bundle.getMessage("FindPanelButtonTurnoutTt"));
229        buttonPanel.add(turnoutButton);
230
231        buttonPanel.setMaximumSize(buttonPanel.getPreferredSize());
232
233        // hook up to receive traffic
234        monitor = new Monitor(model);
235        memo.get(OlcbInterface.class).registerMessageListener(monitor);
236
237        // set options
238        showRequiresLabel.setSelected(pm.getSimplePreferenceState(requireLabelChecked));
239        showRequiresMatch.setSelected(pm.getSimplePreferenceState(requireBothPCChecked));
240        filter();
241    }
242
243    public EventTablePane() {
244        // interface and connections built in initComponents(..)
245    }
246    
247    // load updateMatchGroup combobox with current contents
248    protected void updateMatchGroupName() {
249        matchGroupName.removeAllItems();
250        matchGroupName.addItem(Bundle.getMessage("FrameAllGroups"));
251        
252        var list = groupStore.getGroupNames();
253        for (String group : list) {
254            matchGroupName.addItem(group);
255        }        
256
257        matchGroupName.setVisible(matchGroupName.getItemCount() > 1);
258    }
259
260    String requireLabelChecked = this.getClass().getName() + ".requireLabelChecked"; // NOI18N
261    String requireBothPCChecked = this.getClass().getName() + ".requireBothPCChecked"; // NOI18N
262
263    @Override
264    public void dispose() {
265        // Save the column layout
266        InstanceManager.getOptionalDefault(JmriJTablePersistenceManager.class).ifPresent((tpm) -> {
267           tpm.stopPersisting(table);
268        });
269        // save checkbox properties
270        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
271        pm.setSimplePreferenceState(requireLabelChecked, showRequiresLabel.isSelected());
272        pm.setSimplePreferenceState(requireBothPCChecked, showRequiresMatch.isSelected());
273
274        // remove traffic connection
275        memo.get(OlcbInterface.class).unRegisterMessageListener(monitor);
276        monitor = null;
277        // drop model connections
278        model.dispose();
279        model = null;
280        // and complete this
281        super.dispose();
282    }
283
284    @Override
285    public java.util.List<JMenu> getMenus() {
286        // create a file menu
287        var retval = new ArrayList<JMenu>();
288        var fileMenu = new JMenu(Bundle.getMessage("PaneMenuFile"));
289        fileMenu.setMnemonic(KeyEvent.VK_F);
290        
291        var csvWriteItem = new JMenuItem(Bundle.getMessage("PaneSaveToCsv"), KeyEvent.VK_S);
292        KeyStroke ctrlSKeyStroke = KeyStroke.getKeyStroke("control S");
293        if (jmri.util.SystemType.isMacOSX()) {
294            ctrlSKeyStroke = KeyStroke.getKeyStroke("meta S");
295        }
296        csvWriteItem.setAccelerator(ctrlSKeyStroke);
297        csvWriteItem.addActionListener(this::writeToCsvFile);
298        fileMenu.add(csvWriteItem);
299        
300        var csvReadItem = new JMenuItem(Bundle.getMessage("PaneReadFromCsv"), KeyEvent.VK_O);
301        KeyStroke ctrlOKeyStroke = KeyStroke.getKeyStroke("control O");
302        if (jmri.util.SystemType.isMacOSX()) {
303            ctrlOKeyStroke = KeyStroke.getKeyStroke("meta O");
304        }
305        csvReadItem.setAccelerator(ctrlOKeyStroke);
306        csvReadItem.addActionListener(this::readFromCsvFile);
307        fileMenu.add(csvReadItem);
308        
309        retval.add(fileMenu);
310        return retval;
311    }
312
313    @Override
314    public String getHelpTarget() {
315        return "package.jmri.jmrix.openlcb.swing.eventtable.EventTablePane";
316    }
317
318    @Override
319    public String getTitle() {
320        if (memo != null) {
321            return (memo.getUserName() + " " + Bundle.getMessage("TitleEventTable"));
322        }
323        return getTitle(Bundle.getMessage("TitleEventTable"));
324    }
325
326    public void sendRequestEvents(java.awt.event.ActionEvent e) {
327        model.clearAllPresentNodes();
328
329        model.loadNameStoreEventIDs();
330        model.handleTableUpdate(-1, -1);
331
332        final int IDENTIFY_EVENTS_DELAY = 250; // msec between operations - 250 events at speed
333        int nextDelay = 0;
334
335        // assumes that a VerifyNodes has been done and all nodes are in the MimicNodeStore
336        for (var memo : mimcStore.getNodeMemos()) {
337
338            jmri.util.ThreadingUtil.runOnLayoutDelayed(() -> {
339                var destNodeID = memo.getNodeID();
340                log.trace("send IdentifyEventsAddressedMessage {} {}", nid, destNodeID);
341                Message m = new IdentifyEventsAddressedMessage(nid, destNodeID);
342                connection.put(m, null);
343            }, nextDelay);
344
345            nextDelay += IDENTIFY_EVENTS_DELAY;
346        }
347        // Our reference to the node names in the MimicNodeStore will
348        // trigger a SNIP request if we don't have them yet.  In case that happens
349        // we want to trigger a table refresh to make sure they get displayed.
350        final int REFRESH_INTERVAL = 1000;
351        jmri.util.ThreadingUtil.runOnGUIDelayed(() -> {
352            model.handleTableUpdate(-1,-1);
353        }, nextDelay+REFRESH_INTERVAL);
354        jmri.util.ThreadingUtil.runOnGUIDelayed(() -> {
355            model.handleTableUpdate(-1,-1);
356        }, nextDelay+REFRESH_INTERVAL*2);
357        jmri.util.ThreadingUtil.runOnGUIDelayed(() -> {
358            model.handleTableUpdate(-1,-1);
359        }, nextDelay+REFRESH_INTERVAL*4);
360
361    }
362
363    void popcornButtonChanged() {
364        model.popcornModeActive = popcorn.isSelected();
365        log.debug("Popcorn mode {}", model.popcornModeActive);
366    }
367
368
369    public void findRequested(java.awt.event.ActionEvent e) {
370        var text = findID.getText();
371        // take off all the trailing .00
372        text = text.strip().replaceAll("(.00)*$", "");
373        log.debug("Request find event [{}]", text);
374        // just search event ID
375        table.clearSelection();
376        if (findTextSearch(text, EventTableDataModel.COL_EVENTID)) return;
377    }
378    
379    public void findTextRequested(java.awt.event.ActionEvent e) {
380        String text = findTextID.getText();
381        log.debug("Request find text {}", text);
382        // first search event name, then from config, then producer name, then consumer name
383        table.clearSelection();
384        if (findTextSearch(text, EventTableDataModel.COL_EVENTNAME)) return;
385        if (findTextSearch(text, EventTableDataModel.COL_CONTEXT_INFO)) return;
386        if (findTextSearch(text, EventTableDataModel.COL_PRODUCER_NAME)) return;
387        if (findTextSearch(text, EventTableDataModel.COL_CONSUMER_NAME)) return;
388        return;
389
390        //model.highlightEvent(new EventID(findID.getText()));
391    }
392    
393    protected boolean findTextSearch(String text, int column) {
394        text = text.toUpperCase();
395        try {
396            for (int row = 0; row < model.getRowCount(); row++) {
397                var cell = table.getValueAt(row, column);
398                if (cell == null) continue;
399                var value = cell.toString().toUpperCase();
400                if (value.startsWith(text)) {
401                    table.changeSelection(row, column, false, false);
402                    return true;
403                }
404            }
405        } catch (RuntimeException e) {
406            // we get ArrayIndexOutOfBoundsException occasionally for no known reason
407            log.debug("unexpected AIOOBE");
408        }
409        return false;
410    }
411    
412    public void addRequested(java.awt.event.ActionEvent e) {
413        var text = findID.getText();
414        EventID eventID = new EventID(text);
415        // first, add the event
416        var memo = new EventTableDataModel.TripleMemo(
417                            eventID,
418                            "",
419                            null,
420                            "",
421                            null,
422                            ""
423                        );
424        // check to see if already in there:
425        boolean found = false;
426        for (var check : EventTableDataModel.memos) {
427            if (memo.eventID.equals(check.eventID)) {
428                found = true;
429                break;
430            }
431        }
432        if (! found) {
433            EventTableDataModel.memos.add(memo);
434        }
435        model.fireTableDataChanged();
436        // now select that one
437        findRequested(e);
438        
439    }
440    
441    public void sensorRequested(java.awt.event.ActionEvent e) {
442        // loop over sensors to find the OpenLCB ones
443        var beans = InstanceManager.getDefault(SensorManager.class).getNamedBeanSet();
444        for (NamedBean bean : beans ) {
445            if (bean instanceof OlcbSensor) {
446                oneSensorToTag(true,  bean); // active
447                oneSensorToTag(false, bean); // inactive
448            }
449        }
450    }
451
452    private void oneSensorToTag(boolean isActive, NamedBean bean) {
453        var sensor = (OlcbSensor) bean;
454        var sensorID = sensor.getEventID(isActive);
455        if (! isEventNamePresent(sensorID)) {
456            // add the association
457            nameStore.addMatch(sensorID, sensor.getEventName(isActive));
458        }
459    }
460
461    public void turnoutRequested(java.awt.event.ActionEvent e) {
462        // loop over turnouts to find the OpenLCB ones
463        var beans = InstanceManager.getDefault(TurnoutManager.class).getNamedBeanSet();
464        for (NamedBean bean : beans ) {
465            if (bean instanceof OlcbTurnout) {
466                oneTurnoutToTag(true,  bean); // thrown
467                oneTurnoutToTag(false, bean); // closed
468            }
469        }
470    }
471
472    private void oneTurnoutToTag(boolean isThrown, NamedBean bean) {
473        var turnout = (OlcbTurnout) bean;
474        var turnoutID = turnout.getEventID(isThrown);
475        if (! isEventNamePresent(turnoutID)) {
476            // add the association
477            nameStore.addMatch(turnoutID, turnout.getEventName(isThrown));
478        }
479    }
480    
481    
482    // CSV file chooser
483    // static to remember choice from one use to another.
484    static JFileChooser fileChooser = null;
485
486    /**
487     * Write out contents in CSV form
488     * @param e Needed for signature of method, but ignored here
489     */
490    public void writeToCsvFile(ActionEvent e) {
491        var file = getCsvWriteFile();
492        
493        if (file == null) {
494            log.info("Event Table CVS write aborted");
495            return;
496        }
497        log.debug("start to export to CSV file {}", file);
498
499        model.csvWriteOperation(file);
500    }
501
502    /**
503     * User selection of file for doing CSV write.
504     * @return selected output file or null for aborted
505     */
506    File getCsvWriteFile() {
507        if (fileChooser == null) {
508            fileChooser = new jmri.util.swing.JmriJFileChooser();
509        }
510        fileChooser.setDialogTitle(Bundle.getMessage("PaneSaveCsvFile"));
511        fileChooser.rescanCurrentDirectory();
512        fileChooser.setSelectedFile(new File("eventtable.csv"));
513
514        int retVal = fileChooser.showSaveDialog(this);
515
516        if (retVal == JFileChooser.APPROVE_OPTION) {
517            return fileChooser.getSelectedFile();
518        } else {
519            return null;
520        }
521    }
522    
523    /**
524     * Read event names from a CSV file
525     * @param e Needed for signature of method, but ignored here
526     */
527    public void readFromCsvFile(ActionEvent e) {
528
529        File file = getCsvReadFile();
530        
531        model.csvReadOperation(file);
532    }
533
534    File getCsvReadFile() {
535        if (fileChooser == null) {
536            fileChooser = new jmri.util.swing.JmriJFileChooser();
537        }
538        fileChooser.setDialogTitle(Bundle.getMessage("PaneOpenCsvFile"));
539        fileChooser.rescanCurrentDirectory();
540
541        int retVal = fileChooser.showOpenDialog(this);
542
543        if (retVal == JFileChooser.APPROVE_OPTION) {
544            File file = fileChooser.getSelectedFile();
545            if (log.isDebugEnabled()) {
546                log.debug("start to read from CSV file {}", file);
547            }
548            return file;
549        } else {
550            return null;
551        }
552    }
553    
554    /**
555     * Check whether a Event Name tag is defined or not.
556     * Check for other uses before changing this.
557     * @param eventID EventID in native form
558     * @return true is the event name tag is present
559     */
560    public boolean isEventNamePresent(EventID eventID) {
561        return nameStore.hasEventName(eventID);
562    }
563    
564    /**
565     * Set up filtering of displayed rows
566     */
567    private void filter() {
568        RowFilter<EventTableDataModel, Integer> rf = new RowFilter<EventTableDataModel, Integer>() {
569            /**
570             * @return true if row is to be displayed
571             */
572            @Override
573            public boolean include(RowFilter.Entry<? extends EventTableDataModel, ? extends Integer> entry) {
574
575                int row = entry.getIdentifier();
576
577                var name = model.getValueAt(row, EventTableDataModel.COL_EVENTNAME);
578                if ( showRequiresLabel.isSelected() && (name == null || name.toString().isEmpty()) ) return false;
579
580                if ( showRequiresMatch.isSelected()) {
581                    var memo = model.getTripleMemo(row);
582
583                    if (memo.producer == null && !model.producerPresent(memo.eventID)) {
584                        // no matching producer
585                        return false;
586                    }
587
588                    if (memo.consumer == null && !model.consumerPresent(memo.eventID)) {
589                        // no matching consumer
590                        return false;
591                    }
592                }
593
594                // check for group match
595                if ( matchGroupName.getSelectedIndex() > 0) {  // -1 is empty combobox
596                    String group = matchGroupName.getSelectedItem().toString();
597                    var memo = model.getTripleMemo(row);
598                    if ( (! groupStore.isNodeInGroup(memo.producer, group))
599                        && (! groupStore.isNodeInGroup(memo.consumer, group)) ) {
600                            return false;
601                    }
602                }
603                
604                // passed all filters
605                return true;
606            }
607        };
608        sorter.setRowFilter(rf);
609    }
610
611    /**
612     * Internal class to watch OpenLCB traffic
613     */
614
615    static class Monitor extends MessageDecoder {
616
617        Monitor(EventTableDataModel model) {
618            this.model = model;
619        }
620
621        EventTableDataModel model;
622
623        /**
624         * Handle "Producer/Consumer Event Report" message
625         * @param msg       message to handle
626         * @param sender    connection where it came from
627         */
628        @Override
629        public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender){
630            ThreadingUtil.runOnGUIEventually(()->{
631                var nodeID = msg.getSourceNodeID();
632                var eventID = msg.getEventID();
633                model.recordProducer(eventID, nodeID, "", true);
634                model.highlightProducer(eventID, nodeID);
635            });
636        }
637
638        /**
639         * Handle "Consumer Identified" message
640         * @param msg       message to handle
641         * @param sender    connection where it came from
642         */
643        @Override
644        public void handleConsumerIdentified(ConsumerIdentifiedMessage msg, Connection sender){
645            ThreadingUtil.runOnGUIEventually(()->{
646                var nodeID = msg.getSourceNodeID();
647                var eventID = msg.getEventID();
648                model.recordConsumer(eventID, nodeID, "");
649            });
650        }
651
652        /**
653         * Handle "Producer Identified" message
654         * @param msg       message to handle
655         * @param sender    connection where it came from
656         */
657        @Override
658        public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender){
659            ThreadingUtil.runOnGUIEventually(()->{
660                var nodeID = msg.getSourceNodeID();
661                var eventID = msg.getEventID();
662                model.recordProducer(eventID, nodeID, "", false);
663            });
664        }
665
666        @Override
667        public void handleConsumerRangeIdentified(ConsumerRangeIdentifiedMessage msg, Connection sender){
668            ThreadingUtil.runOnGUIEventually(()->{
669                final var nodeID = msg.getSourceNodeID();
670                final var eventID = msg.getEventID();
671                
672                final long rangeSuffix = eventID.rangeSuffix();
673                // have to set low part of event ID to 0's as it might be 1's
674                EventID zeroedEID = new EventID(eventID.toLong() & (~rangeSuffix));
675                
676                model.recordConsumer(zeroedEID, nodeID, (new EventID(eventID.toLong() | rangeSuffix)).toShortString());
677            });
678        }
679    
680        @Override
681        public void handleProducerRangeIdentified(ProducerRangeIdentifiedMessage msg, Connection sender){
682            ThreadingUtil.runOnGUIEventually(()->{
683                final var nodeID = msg.getSourceNodeID();
684                final var eventID = msg.getEventID();
685                
686                final long rangeSuffix = eventID.rangeSuffix();
687                // have to set low part of event ID to 0's as it might be 1's
688                EventID zeroedEID = new EventID(eventID.toLong() & (~rangeSuffix));
689                
690                model.recordProducer(zeroedEID, nodeID, (new EventID(eventID.toLong() | rangeSuffix)).toShortString(), false);
691            });
692        }
693
694        /*
695         * We no longer handle "Simple Node Ident Info Reply" messages because of
696         * excessive redisplays.  Instead, we expect the MimicNodeStore to handle
697         * these and provide the information when requested.
698         */
699    }
700
701    /**
702     * Nested class to create one of these using old-style defaults
703     */
704    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
705
706        public Default() {
707            super("LCC Event Table",
708                    new jmri.util.swing.sdi.JmriJFrameInterface(),
709                    EventTablePane.class.getName(),
710                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
711        }
712        
713        public Default(String name, jmri.util.swing.WindowInterface iface) {
714            super(name,
715                    iface,
716                    EventTablePane.class.getName(),
717                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));        
718        }
719
720        public Default(String name, Icon icon, jmri.util.swing.WindowInterface iface) {
721            super(name,
722                    icon, iface,
723                    EventTablePane.class.getName(),
724                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));        
725        }
726    }
727    
728    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EventTablePane.class);
729}