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}