001package jmri.jmrix.rfid.swing.tagcarwin;
002
003import jmri.InstanceManager;
004import jmri.UserPreferencesManager;
005import jmri.jmrit.operations.locations.LocationManager;
006import jmri.jmrit.operations.rollingstock.RollingStock;
007import jmri.jmrit.operations.rollingstock.cars.Car;
008import jmri.jmrit.operations.rollingstock.cars.CarManager;
009import jmri.jmrit.operations.trains.Train;
010import jmri.jmrix.rfid.RfidListener;
011import jmri.jmrix.rfid.RfidMessage;
012import jmri.jmrix.rfid.RfidReply;
013import jmri.jmrix.rfid.RfidSystemConnectionMemo;
014import jmri.jmrix.rfid.swing.RfidPanelInterface;
015import jmri.util.swing.JmriPanel;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019import javax.swing.*;
020import javax.swing.event.TableModelEvent;
021import javax.swing.event.TableModelListener;
022import java.awt.*;
023import java.awt.event.ActionListener;
024import java.time.LocalTime;
025import java.util.ArrayList;
026
027/**
028 * A monitor for RFID tags which shows the tag, a car (if there is a car associated with that tag).
029 * If there is no car, a button allows the user to associate a car. For those tags with cars,
030 * the user can set the location or edit the car.
031 *
032 * @author J. Scott Walton Copyright (C) 2022
033 */
034public class TagMonitorPane extends JmriPanel implements RfidListener, RfidPanelInterface, TableModelListener {
035    private static final Logger log = LoggerFactory.getLogger(TagMonitorPane.class);
036    // panel members
037    TableDataModel dataModel = null;
038    protected Integer currentRowCount = 15;
039    private String lastTagSeen = "";
040    private final ArrayList<RollingStock> lastTrainCars = new ArrayList<>();
041    private Train lastTrain = null;
042
043    CarManager carManager = InstanceManager.getDefault(CarManager.class);
044    LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
045    RfidSystemConnectionMemo memo = null;
046
047
048    /**
049     * If the System Prefix is available, append it to the title to identify which reader this belongs to
050     * @return the title for this panel
051     */
052    @Override
053    public String getTitle() {
054        if (memo != null) {
055            return Bundle.getMessage("MonitorRFIDTagCars", "RFID Device") + " - " + memo.getSystemPrefix();
056        } else {
057            return Bundle.getMessage("MonitorRFIDTagCars", "RFID Device");
058        }
059    }
060
061    /**
062     * RFID typically don't send messages, so this is ignored
063     * @param m the message received
064     */
065    @Override
066    public void message(RfidMessage m) {
067        log.debug("got a new tag {}", m.toString());
068    }
069
070
071    /**
072     * Process an RFID message (typically a tag was seen)
073     * Tags in JMRI are identified with the string "ID" + the tag number
074     * if this is the same tag as was last seen, suppress the display unless the option
075     * to display duplicates has been set (in that case, update the timestamp)
076     * Pass the tag to createNewItem to build the new display
077     * @param m the message
078     */
079    @Override
080    public void reply(RfidReply m) {
081        log.debug("got a new Reply msg {}", m.toString());
082        String thisTag = "ID" + memo.getProtocol().getTag(m);
083        log.debug("This this tag is -{}-", thisTag);
084        if (!showDuplicates.isSelected()) {
085            if (thisTag.equals(lastTagSeen)) {
086                dataModel.setLast(LocalTime.now());
087                return;
088            }
089        }
090        lastTagSeen = thisTag;
091        createNewItem(thisTag);
092    }
093
094
095    /**
096     * Create most of the new row data
097     * Look up the car, if found we add the car to row, and set up most of the data
098     * the combo boxes will be added in the data model
099     * @param tag the value read from the RFID reader
100     * @return the row created to represent the row in the table
101     */
102    private TagCarItem createNewItem(String tag) {
103        TagCarItem newTag = new TagCarItem(tag, LocalTime.now());
104        Car thisCar = carManager.getByRfid(tag);
105        JButton action1Button = new JButton();
106        JButton action2 = new JButton();
107        newTag.setAction2(action2);
108        newTag.setAction1(action1Button);
109        if (thisCar != null) {
110            newTag.setRoad(thisCar.getRoadName());
111            newTag.setCarNumber(thisCar.getNumber());
112            newTag.setCurrentCar(thisCar);
113            action1Button.setText(Bundle.getMessage("MonitorSetLocation"));
114            action1Button.setEnabled(false); // not enabled until location is changed
115            action1Button.setToolTipText(Bundle.getMessage("MonitorSetLocToolTip"));
116            action2.setText(Bundle.getMessage("MonitorEditCar"));
117            action2.setToolTipText(Bundle.getMessage("MonitorEditToolTip"));
118            action2.setEnabled(true);
119            if (thisCar.getTrainName() != null) {
120                newTag.setTrain(thisCar.getTrainName());
121                newTag.setTrainPosition(getCarTrainPosition(thisCar, thisCar.getTrain()));
122            }
123        } else {
124            action1Button.setText(Bundle.getMessage("MonitorAssociate"));
125            action1Button.setToolTipText(Bundle.getMessage("MonitorAssociateToolTip"));
126            action2.setText("");
127            action1Button.setEnabled(true);
128            action2.setEnabled(false);
129        }
130        dataModel.add(newTag);
131        return newTag;
132    }
133
134    /**
135     * If this car (engine or car) is in a train, determine what the car position is
136     * @param thisCar the car we are looking for
137     * @param thisTrain the train to find it in
138     * @return the position of car within the train
139     */
140    public int getCarTrainPosition(RollingStock thisCar, Train thisTrain) {
141        String carRoad = thisCar.getRoadName();
142        String carNumber = thisCar.getNumber();
143        if (thisTrain == null) {
144            log.debug("train is null in getCarTrainPosition");
145            return 0;
146        }
147        log.debug("finding car {} {} in Train {}", carRoad, carNumber, thisTrain.getName());
148        if (!thisTrain.equals(lastTrain)) {
149            log.debug("new train - retrieving it");
150            lastTrain = thisTrain;
151            lastTrainCars.clear();
152            lastTrainCars.addAll(carManager.getByTrainDestinationList(thisTrain));
153        }
154        int positionCounter = 0;
155        for (RollingStock trainElement : lastTrainCars) {
156            if (trainElement instanceof Car) {
157                positionCounter++;
158                if (trainElement.equals(thisCar)) {
159                    return positionCounter;
160                }
161            }
162        }
163        log.error("Expected to find car {} {} in train {} and did not", carRoad, carNumber, thisTrain.getName());
164        return 0;
165    }
166
167    /**
168     * Save the connection identifier to use for the System Prefix and to get the tag protocol
169     * add this class as a listener to get the RFID replies
170     * @param memo SystemConnectionMemo for configured RFID system
171     */
172    @Override
173    public void initComponents(RfidSystemConnectionMemo memo) {
174        this.memo = memo;
175
176        memo.getTrafficController().addRfidListener(this);
177        log.debug("added self as RFID listener");
178    }
179
180
181    /**
182     * Save the preferences for use later
183     * They will apply to all instances of this class (regardless of which connections use it)
184     */
185    @Override
186    public void dispose() {
187        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
188        pm.setSimplePreferenceState(timeCheck, showTimestamps.isSelected());
189        pm.setSimplePreferenceState(dupeCheck, showDuplicates.isSelected());
190        pm.setProperty(rowCountField, rowCountField, rowCount.getText());
191        pm.setSimplePreferenceState(forceSet, forceSetCar.isSelected());
192        super.dispose();
193    }
194
195    /**
196     * Replace the current message on the panel with a new one
197     * Also sets the color to black (in case it was set to read after an error)
198     * @param newMessage the message to be displayed at the bottom of the panel
199     */
200    public void setMessageNormal(String newMessage) {
201        panelMessage.setForeground(Color.BLACK);
202        panelMessage.setText(newMessage);
203    }
204
205    /**
206     * Set the message at the bottom of the panel and change the color to red to highlight it
207     * after an error has occurred
208     * @param newMessage the message to be displayed in RED
209     */
210    public void setMessageError(String newMessage) {
211        setForeground(Color.RED);
212        panelMessage.setText(newMessage);
213    }
214
215    // elements for the UI
216    protected JCheckBox showTimestamps = new JCheckBox();
217    protected JCheckBox showDuplicates = new JCheckBox();
218    protected JCheckBox forceSetCar = new JCheckBox();
219    protected String timeCheck = this.getClass().getName() + "Times";
220    protected String dupeCheck = this.getClass().getName() + "Duplicates";
221    protected String forceSet = this.getClass().getName() + "ForceSet";
222    protected JButton clearButton = new JButton();
223    protected JLabel rowCountLabel = new JLabel();
224    protected JTextField rowCount = new JTextField();
225    protected JButton setRowCount = new JButton();
226    protected String rowCountField = this.getClass().getName() + "RowCount";
227    JLabel panelMessage = new JLabel("");
228
229    public boolean getShowTimestamps() {
230        return showTimestamps.isSelected();
231    }
232
233    @Override
234    public void initComponents() {
235        dataModel = new TableDataModel(this);
236        JTable tagMonitorTable = new JTable(dataModel);
237        tagMonitorTable.setRowHeight(tagMonitorTable.getRowHeight() + 5);
238        tagMonitorTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
239        tagMonitorTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
240        dataModel.setParent(tagMonitorTable);
241        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
242        showTimestamps.setText(Bundle.getMessage("MonitorTimestamps"));
243        showTimestamps.setVisible(true);
244        showTimestamps.setToolTipText(Bundle.getMessage("MonitorTimeToolTip"));
245        showTimestamps.setSelected(pm.getSimplePreferenceState(timeCheck));
246        showDuplicates.setText(Bundle.getMessage("MonitorShowDupes"));
247        showDuplicates.setVisible(true);
248        showDuplicates.setToolTipText(Bundle.getMessage("MonitorDupesToolTip"));
249        showDuplicates.setSelected(pm.getSimplePreferenceState(dupeCheck));
250        forceSetCar.setText(Bundle.getMessage("MonitorForceSet"));
251        forceSetCar.setSelected(pm.getSimplePreferenceState(forceSet));
252        dataModel.showTimestamps = showTimestamps.isSelected();
253        dataModel.setForceSetLocation(forceSetCar.isSelected());
254
255        dataModel.initTable();
256        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
257        JScrollPane tablePane = new JScrollPane(tagMonitorTable);
258        tablePane.setPreferredSize(new Dimension(100, 400));
259        tagMonitorTable.setFillsViewportHeight(true);
260        add(tablePane);
261        JPanel checkBoxPanel = new JmriPanel();
262        JPanel messagePanel = new JmriPanel();
263        messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.X_AXIS));
264        JLabel msgLabel = new JLabel(Bundle.getMessage("MonitorMessageLabel"));
265        msgLabel.setPreferredSize(new Dimension(30, 15));
266        messagePanel.add(msgLabel);
267        panelMessage.setText(""); // no message to start
268        panelMessage.setPreferredSize(new Dimension(170, 15));
269        messagePanel.add(panelMessage);
270        add(messagePanel);
271        checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.X_AXIS));
272        clearButton.setText(Bundle.getMessage("ButtonClearScreen"));
273        checkBoxPanel.add(clearButton);
274        clearButton.addActionListener(e -> {
275            dataModel.clearTable();
276        });
277        rowCountLabel.setText(Bundle.getMessage("MonitorRowCount"));
278        checkBoxPanel.add(rowCountLabel);
279        try {
280            currentRowCount = Integer.valueOf(pm.getProperty(rowCountField, rowCountField).toString());
281        } catch (NullPointerException nulls) {
282            currentRowCount = 15;
283        }
284        dataModel.setRowMax(currentRowCount);
285        checkBoxPanel.setPreferredSize(new Dimension(750, 30));
286        checkBoxPanel.setMaximumSize(new Dimension(800, 30));
287        rowCount.setPreferredSize(new Dimension(40, 15));
288        rowCount.setMinimumSize(new Dimension( 40, 12));
289        rowCount.setText(currentRowCount.toString());
290        checkBoxPanel.add(rowCount);
291        checkBoxPanel.add(setRowCount);
292        setRowCount.setText(Bundle.getMessage("MonitorSetRowCount"));
293        setRowCount.setToolTipText(Bundle.getMessage("MonitorRowToolTip"));
294        checkBoxPanel.add(showTimestamps);
295        showTimestamps.setMinimumSize(new Dimension(60,12));
296        checkBoxPanel.add(showDuplicates);
297        showDuplicates.setMinimumSize(new Dimension( 60, 12));
298        add(checkBoxPanel);
299        rowCount.addActionListener(e -> {
300            if (!rowCount.getText().isEmpty()) {
301                String text = rowCount.getText();
302                try {
303                    int newValue = Integer.parseInt(text);
304                    if (newValue > 0 && newValue < 100) {
305                        setRowCount.setEnabled(true);
306                        setMessageNormal("");
307                    } else {
308                        setMessageError(Bundle.getMessage("MonitorRowCountError"));
309                        setRowCount.setEnabled(false);
310                    }
311                } catch (NumberFormatException exception) {
312                    setMessageError(Bundle.getMessage("MonitorRowCountError"));
313                    setRowCount.setEnabled(false);
314                }
315            } else {
316                setMessageNormal("");
317                setRowCount.setEnabled(false);
318            }
319        });
320        setRowCount.addActionListener(e -> {
321            try {
322                int newValue = Integer.parseInt(rowCount.getText());
323                dataModel.setRowMax(newValue);
324                setMessageNormal("");
325            } catch (NumberFormatException exception) {
326                setMessageError(Bundle.getMessage("MonitorRowCountError"));
327                log.error("error interpreting new number of lines");
328            }
329        });
330        ActionListener checkListener = e -> {
331            if (e.getSource().equals(showTimestamps)) {
332                dataModel.showTimestamps(showTimestamps.isSelected());
333                log.debug("switching show timestamps now");
334            } else if (e.getSource().equals(showDuplicates)) {
335                log.debug("changing show duplicates now");
336            }
337        };
338        showDuplicates.addActionListener(checkListener);
339        showTimestamps.addActionListener(checkListener);
340        dataModel.addTableModelListener(this);
341    }
342
343    @Override
344    public void tableChanged(TableModelEvent e) {
345        if (e.getType() == TableModelEvent.UPDATE) {
346            log.debug("table was updated");
347            int column = e.getColumn();
348            if (column == TableDataModel.LOCATION_COLUMN) {
349                log.debug("Location was changed");
350                int thisRow = e.getFirstRow();
351                while (thisRow <= e.getLastRow()) {
352                    log.debug("Updated location column row {}", thisRow);
353                    thisRow++;
354                }
355            } else if (column == TableDataModel.TRACK_COLUMN) {
356                int thisRow = e.getFirstRow();
357                while (thisRow <= e.getLastRow()) {
358                    log.debug("Track column row {}", thisRow);
359                    thisRow++;
360                }
361            }
362        }
363    }
364
365}