001package jmri.jmrix.rfid.swing.tagcarwin; 002 003import jmri.jmrit.operations.locations.Location; 004import jmri.jmrit.operations.locations.Track; 005import jmri.jmrit.operations.rollingstock.RollingStock; 006import jmri.jmrit.operations.rollingstock.cars.Car; 007import jmri.jmrit.operations.rollingstock.cars.gui.CarEditFrame; 008import jmri.util.swing.XTableColumnModel; 009import jmri.util.table.ButtonRenderer; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013import javax.swing.*; 014import javax.swing.table.TableCellEditor; 015import java.awt.*; 016import java.awt.event.ActionEvent; 017import java.awt.event.ActionListener; 018import java.awt.event.ItemEvent; 019import java.awt.event.ItemListener; 020import java.beans.PropertyChangeEvent; 021import java.beans.PropertyChangeListener; 022import java.time.LocalTime; 023import java.util.ArrayList; 024import java.util.Hashtable; 025import java.util.List; 026import java.util.Vector; 027 028/** 029 * The table model for displaying rows of incoming RFID tags and associating them with cars 030 * and locations. This is where most of the logic resides, though the actually receiving of the table 031 * is done in the parent 032 * 033 * @author J. Scott Walton Copyright (C) 2022 034 */ 035public class TableDataModel extends javax.swing.table.AbstractTableModel implements PropertyChangeListener, ItemListener, ActionListener { 036 037 private static final Logger log = LoggerFactory.getLogger(TableDataModel.class); 038 039 protected JTable tableParent = null; 040 protected static final int TIME_COLUMN = 0; 041 protected static final int ROAD_COLUMN = 1; 042 protected static final int CAR_NUMBER_COLUMN = 2; 043 protected static final int TAG_COLUMN = 3; 044 protected static final int LOCATION_COLUMN = 4; 045 protected static final int TRACK_COLUMN = 5; 046 protected static final int TRAIN_COLUMN = 6; 047 protected static final int TRAIN_POSITION_COLUMN = 7; 048 protected static final int DESTINATION_COLUMN = 8; 049 protected static final int ACTION1_COLUMN = 9; 050 protected static final int ACTION2_COLUMN = 10; 051 protected static final int COLUMN_COUNT = ACTION2_COLUMN + 1; 052 private final int[] tableColumn_widths = {60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60}; 053 054 // this is the list the represents the JTable 055 List<TagCarItem> tagList = new Vector<>(); 056 057 protected List<String> locations; 058 protected Hashtable<String, List<String>> trackLists; 059 TagMonitorPane parentPane = null; 060 CarEditFrame cef; 061 AssociateFrame associateFrame; 062 063 public void setForceSetLocation(boolean forceSetLocation) { 064 this.forceSetLocation = forceSetLocation; 065 } 066 067 boolean forceSetLocation = false; 068 private boolean addingRow = false; // set true when in the middle of adding a row 069 070 public TableDataModel(TagMonitorPane parentPane) { 071 super(); 072 this.parentPane = parentPane; 073 } 074 075 public TableDataModel() { 076 super(); 077 } 078 079 public void showTimestamps(boolean showTimestamps) { 080 this.showTimestamps = showTimestamps; 081 XTableColumnModel tcm = (XTableColumnModel) tableParent.getColumnModel(); 082 tcm.setColumnVisible(tcm.getColumnByModelIndex(TIME_COLUMN), showTimestamps); 083 fireTableDataChanged(); 084 } 085 086 protected boolean showTimestamps = false; 087 088 public void setRowMax(int rowMax) { 089 cleanTable(rowMax, true); 090 this.rowMax = rowMax; 091 } 092 093 094 /** 095 * @param locationBox - the combBox to be used for the Location 096 * @param trackBox the ComboBox for the track 097 * @param car fill in the list of locations in the location box if the car has a location, select it when 098 * the location item is selected, fill in the track box with the approriate list select the 099 * track 100 */ 101 private void setCombos(JComboBox<String> locationBox, JComboBox<String> trackBox, RollingStock car) { 102 locationBox.removeItemListener(this); 103 trackBox.removeItemListener(this); 104 locationBox.addItem(""); 105 for (String loc : locations) { 106 locationBox.addItem(loc); 107 } 108 String locName; 109 if (car.getLocation() == null) { 110 locationBox.setSelectedIndex(0); 111 trackBox.addItem(""); 112 trackBox.setEnabled(false); 113 // track box should not have an item listener if it is not enabled 114 } else { 115 locationBox.addItemListener(this); 116 trackBox.addItemListener(this); 117 locName = car.getLocationName(); 118 locationBox.setSelectedItem(locName); 119 List<String> tracksHere = trackLists.get(locName); 120 for (String thisTrack : tracksHere) { 121 trackBox.addItem(thisTrack); 122 } 123 if (car.getTrack() != null) { 124 trackBox.setSelectedItem(car.getTrack().getName()); 125 } 126 if (tracksHere.size() == 2) { 127 trackBox.setSelectedIndex(1); 128 } 129 trackBox.addItemListener(this); 130 } 131 locationBox.addItemListener(this); 132 } 133 134 public void clearTable() { 135 log.debug("clearing the RFID tag car window"); 136 cleanTable(0, true); 137 } 138 139 private void cleanTable(int newRowMax, boolean fireChange) { 140 boolean rowsRemoved = false; 141 if (tagList.size() <= newRowMax) { 142 return; 143 } 144 if (tableParent.isEditing()) { 145 tableParent.getCellEditor().stopCellEditing(); 146 } 147 while (tagList.size() > newRowMax) { 148 tagList.remove(0); 149 rowsRemoved = true; 150 } 151 if (rowsRemoved && fireChange) { 152 fireTableDataChanged(); 153 } 154 } 155 156 public void add(TagCarItem newItem) { 157 addingRow = true; 158 cleanTable(rowMax - 1, false); 159 RollingStock newCar = newItem.getCurrentCar(); 160 newItem.getAction1().addActionListener(this); 161 if (newCar != null) { 162 // only have a combo box if there is a car - need to build both combo boxes here 163 JComboBox<String> carLocation = new JComboBox<>(); 164 JComboBox<String> carTrack = new JComboBox<>(); 165 setCombos(carLocation, carTrack, newCar); 166 newItem.setLocation(carLocation); 167 newItem.setTrack(carTrack); 168 newItem.getAction2().addActionListener(this); 169 } 170 tagList.add(newItem); 171 parentPane.setMessageNormal(""); 172 fireTableDataChanged(); 173 addingRow = false; 174 } 175 176 public void setLast(LocalTime newLast) { 177 if (tagList.size() > 0) { 178 tagList.get(tagList.size() - 1).setLastSeen(newLast); 179 fireTableCellUpdated(tagList.size()-1, TIME_COLUMN); 180 } 181 } 182 183 private int rowMax = 20; 184 185 public void setParent(JTable parent) { 186 this.tableParent = parent; 187 } 188 189 @Override 190 public void propertyChange(PropertyChangeEvent evt) { 191 192 } 193 194 @Override 195 public int getRowCount() { 196 return tagList.size(); 197 } 198 199 @Override 200 public int getColumnCount() { 201 return COLUMN_COUNT; 202 } 203 204 @Override 205 public Object getValueAt(int rowIndex, int columnIndex) { 206 if (rowIndex >= tagList.size()) { 207 return ""; 208 } 209 TagCarItem current = tagList.get(rowIndex); 210 switch (columnIndex) { 211 case TIME_COLUMN: 212 return current.getLastSeen().toString(); 213 case ROAD_COLUMN: 214 return current.getRoad(); 215 case CAR_NUMBER_COLUMN: 216 return current.getCarNumber(); 217 case TAG_COLUMN: 218 return current.getTag(); 219 case LOCATION_COLUMN: 220 if (current.getCurrentCar() == null) { 221 return ""; 222 } 223 if (current.getLocationCombo().getSelectedIndex() == -1) { 224 return ""; 225 } 226 return current.getLocationCombo().getSelectedItem(); 227 case TRACK_COLUMN: 228 if (current.getCurrentCar() == null) { 229 return ""; 230 } 231 if (current.getLocationCombo().getSelectedIndex() == -1) { 232 return ""; 233 } 234 return current.getTrackCombo().getSelectedItem(); 235 case TRAIN_COLUMN: 236 if (current.getTrain() == null) { 237 return ""; 238 } 239 return current.getTrain(); 240 case DESTINATION_COLUMN: 241 if (current.getDestination() == null) { 242 return ""; 243 } 244 return current.getDestination(); 245 case TRAIN_POSITION_COLUMN: 246 if (current.getTrainPosition() == null) { 247 return ""; 248 } 249 return current.getTrainPosition(); 250 case ACTION1_COLUMN: 251 return current.getAction1().getText(); 252 case ACTION2_COLUMN: 253 if (current.getAction2() == null) { 254 return ""; 255 } 256 return current.getAction2().getText(); 257 default: 258 return "unknown"; //NOI18N 259 } 260 } 261 262 263 264 private void setCarLocation(RollingStock car, TagCarItem thisRow) { 265 if (car == null) { 266 log.error("attempting to set the location of a null car"); 267 return; 268 } 269 log.debug("Setting location of car {} - {}", car.getRoadName(), car.getNumber()); 270 if (!thisRow.isLocationReady()) { 271 log.error("should not be here - this row is not yet ready"); 272 return; 273 } 274 String retValue; 275 if (thisRow.getTempLocation().equals("")) { 276 // removing location from this car 277 278 retValue = car.setLocation(null, null); 279 } else { 280 // adding or replacing the location on this car 281 Location thisLocation = parentPane.locationManager.getLocationByName(thisRow.getTempLocation()); 282 if (thisLocation == null) { 283 log.error("Did not find location identified in ComboBox - {}", thisRow.getTempLocation()); 284 return; 285 } 286 Track thisTrack = null; 287 for (Track track : thisLocation.getTracksList()) { 288 if (track.getName().equals(thisRow.getTempTrack())) { 289 thisTrack = track; 290 break; 291 } 292 } 293 if (thisTrack == null) { 294 log.error("Did not find expected track at this location L - T -- {} - {}", thisRow.getTempLocation(), thisRow.getTempTrack()); 295 return; 296 } 297 retValue = car.setLocation(thisLocation, thisTrack, forceSetLocation); 298 } 299 if (retValue.equals("okay")) { 300 parentPane.setMessageNormal(Bundle.getMessage("MonitorLocationSet")); 301 thisRow.resetTempValues(); 302 } else { 303 parentPane.setMessageError("MonitorLocationFailed"); 304 } 305 } 306 307 private void doEditCar(RollingStock thisCar) { 308 if (cef != null) { 309 cef.dispose(); 310 } 311 SwingUtilities.invokeLater(() -> { 312 cef = new CarEditFrame(); 313 cef.initComponents(); 314 cef.load((Car) thisCar); 315 }); 316 // tableParent.getCellEditor().stopCellEditing(); 317 } 318 319 private void doSetTag(String thisTag, TagCarItem thisRow) { 320 // associate tag 321 if (associateFrame != null) { 322 associateFrame.dispose(); 323 } 324 SwingUtilities.invokeLater(() -> new AssociateFrame(new AssociateTag(thisTag), 325 Bundle.getMessage("AssociateTitle") + " " + thisTag).initComponents()); 326 } 327 328 @Override 329 public void setValueAt(Object value, int row, int col) { 330 TagCarItem thisRowValue = tagList.get(row); 331 switch (col) { 332 case LOCATION_COLUMN: 333 if (value instanceof String) { 334 log.debug("new value for Location column - {}", value); 335 locationItemUpdated(thisRowValue, (String) value); 336 } 337 break; 338 case TRACK_COLUMN: 339 if (value instanceof String) { 340 trackItemUpdate(thisRowValue, (String) value); 341 } 342 break; 343 case ACTION1_COLUMN: 344 log.debug("setValueAt for Action1 column"); 345 return; 346 case ACTION2_COLUMN: 347 log.debug("setValueAt for Action2 column"); 348 break; 349 default: 350 log.error("should not be setting value for column {}", col); 351 } 352 353 } 354 355 Component getLocationRowEditor(JTable table, Object value, boolean isSelected, int row, int column) { 356 int tempCol = column; 357 if (!parentPane.getShowTimestamps()) { 358 tempCol = column + 1; // if timestamp column is not visible , need to adjust count 359 } 360 if (tempCol != LOCATION_COLUMN) { 361 log.error("getLocationRowEditor called for other than Location column {}", column); 362 return null; 363 } 364 if (tagList.get(row).getCurrentCar() == null) { 365 log.debug("this row does not have a car associated -- cannot edit"); 366 JComboBox<String> newBox = new JComboBox<>(); 367 newBox.addItem(""); 368 return newBox; 369 } 370 return tagList.get(row).getLocationCombo(); 371 372 } 373 374 Component getTrackRowEditor(JTable table, Object value, boolean isSelected, int row, int column) { 375 int tempCol = column; 376 if (!parentPane.getShowTimestamps()) { 377 tempCol = column + 1; // if timestamp column is not visible, need to adjust column number to account for it 378 } 379 if (tempCol != TRACK_COLUMN) { 380 log.error("Track row column called for incorrect column: {}", column); 381 return null; 382 } 383 return tagList.get(row).getTrackCombo(); 384 } 385 386 private void buildLocationValues() { 387 locations = new ArrayList<>(); 388 trackLists = new Hashtable<>(); 389 for (Location loc : parentPane.locationManager.getList()) { 390 locations.add(loc.getName()); 391 List<Track> listOfTracks = loc.getTracksByNameList(null); 392 List<String> tempTrack = new ArrayList<>(); 393 tempTrack.add(""); // always have the empty list (not selected) 394 for (Track thisTrack : listOfTracks) { 395 tempTrack.add(thisTrack.getName()); 396 } 397 java.util.Collections.sort(tempTrack); 398 trackLists.put(loc.getName(), tempTrack); 399 } 400 java.util.Collections.sort(locations); 401 402 } 403 404 void initTable() { 405 buildLocationValues(); 406 XTableColumnModel tcm = new XTableColumnModel(); 407 tableParent.setColumnModel(tcm); 408 tableParent.createDefaultColumnsFromModel(); 409 for (int i = 0; i < tcm.getColumnCount(); i++) { 410 tcm.getColumn(i).setPreferredWidth(tableColumn_widths[i]); 411 } 412 ButtonRenderer buttonRenderer = new ButtonRenderer(); 413 tcm.getColumn(ACTION1_COLUMN).setCellRenderer(buttonRenderer); 414 tcm.getColumn(ACTION2_COLUMN).setCellRenderer(buttonRenderer); 415 tcm.setColumnVisible(tcm.getColumnByModelIndex(TIME_COLUMN), showTimestamps); 416 TableCellEditor locationCellEditor = new EditTrackCellEditor(this); 417 TableCellEditor trackCellEditor = new EditTrackCellEditor(this); 418 tcm.getColumnByModelIndex(LOCATION_COLUMN).setCellEditor(locationCellEditor); 419 tcm.getColumnByModelIndex(TRACK_COLUMN).setCellEditor(trackCellEditor); 420 tcm.getColumnByModelIndex(ACTION1_COLUMN).setCellEditor(new EditTrackCellEditor(this)); 421 tcm.getColumnByModelIndex(ACTION2_COLUMN).setCellEditor(new EditTrackCellEditor(this)); 422 fireTableDataChanged(); 423 } 424 425 @Override 426 public String getColumnName(int col) { 427 switch (col) { 428 case TIME_COLUMN: 429 return Bundle.getMessage("MonitorTimeStampCol"); 430 case ROAD_COLUMN: 431 return Bundle.getMessage("MonitorRoadCol"); 432 case CAR_NUMBER_COLUMN: 433 return Bundle.getMessage("MonitorCarNumCol"); 434 case TAG_COLUMN: 435 return Bundle.getMessage("MonitorTagCol"); 436 case LOCATION_COLUMN: 437 return Bundle.getMessage("MonitorLocation"); 438 case TRACK_COLUMN: 439 return Bundle.getMessage("MonitorTrack"); 440 case TRAIN_COLUMN: 441 return Bundle.getMessage("MonitorTrain"); 442 case TRAIN_POSITION_COLUMN: 443 return Bundle.getMessage("MonitorTrainPosition"); 444 case DESTINATION_COLUMN: 445 return Bundle.getMessage("MonitorDestination"); 446 case ACTION1_COLUMN: 447 return Bundle.getMessage("MonitorAction1"); 448 case ACTION2_COLUMN: 449 return Bundle.getMessage("MonitorAction2"); 450 default: 451 return "unknown"; //NOI18N 452 } 453 } 454 455 @Override 456 public Class<?> getColumnClass(int col) { 457 switch (col) { 458 case TRAIN_POSITION_COLUMN: 459 case CAR_NUMBER_COLUMN: 460 return Integer.class; 461 case LOCATION_COLUMN: 462 case TRACK_COLUMN: 463 return JComboBox.class; 464 case ACTION1_COLUMN: 465 case ACTION2_COLUMN: 466 return JButton.class; 467 default: 468 return String.class; 469 } 470 } 471 472 @Override 473 public boolean isCellEditable(int row, int col) { 474 switch (col) { 475 case LOCATION_COLUMN: 476 case TRACK_COLUMN: 477 case ACTION1_COLUMN: 478 case ACTION2_COLUMN: 479 return true; 480 default: 481 return false; 482 } 483 } 484 485 private void locationItemUpdated(TagCarItem thisRow, String newvalue) { 486 if (newvalue.equals("")) { 487 if (thisRow.getLocationValue() == null) { 488 return; 489 } 490 thisRow.setUpdatedLocation("", ""); 491 thisRow.getTrackCombo().setEnabled(false); 492 } else { 493 thisRow.getTrackCombo().removeItemListener(this); 494 List<String> tracksHere = trackLists.get(newvalue); 495 thisRow.getTrackCombo().removeAllItems(); 496 thisRow.getTrackCombo().setEnabled(true); 497 for (String thisTrack : tracksHere) { 498 thisRow.getTrackCombo().addItem(thisTrack); 499 } 500 if (tracksHere.size() == 2) { 501 thisRow.getTrackCombo().setSelectedIndex(1); 502 thisRow.setUpdatedLocation(newvalue, tracksHere.get(1)); 503 if (thisRow.isLocationReady()) { 504 parentPane.setMessageNormal(Bundle.getMessage("MonitorReadyToSet")); 505 thisRow.getAction1().setEnabled(true); 506 } else { 507 parentPane.setMessageNormal("MonitorSetTrackMsg"); 508 thisRow.getAction1().setEnabled(false); 509 } 510 } else { 511 thisRow.setUpdatedLocation(newvalue, ""); 512 parentPane.setMessageNormal(Bundle.getMessage("MonitorSetTrackMsg")); 513 thisRow.getAction1().setEnabled(false); 514 } 515 thisRow.getTrackCombo().addItemListener(this); 516 } 517 518 } 519 520 private void trackItemUpdate(TagCarItem thisRow, String newValue) { 521 thisRow.setUpdatedTrack(newValue); 522 if (thisRow.isLocationReady()) { 523 parentPane.setMessageNormal(Bundle.getMessage("MonitorReadyToSet")); 524 thisRow.getAction1().setEnabled(true); 525 } else { 526 thisRow.getAction1().setEnabled(false); 527 } 528 } 529 530 @Override 531 public void itemStateChanged(ItemEvent e) { 532 log.debug("item event fired for {} ", e.getItem()); 533 if (addingRow || e.getStateChange() != ItemEvent.SELECTED) { 534 // we only need to do something if the location of track was selected 535 return; 536 } 537 for (TagCarItem thisRow : tagList) { 538 if (e.getSource().equals(thisRow.getLocationCombo())) { 539 locationItemUpdated(thisRow, (String) e.getItem()); 540 } else if (e.getSource().equals(thisRow.getTrackCombo())) { 541 trackItemUpdate(thisRow, (String) e.getItem()); 542 } else { 543 log.error("got an ItemEvent for an unknown source"); 544 } 545 } 546 } 547 548 @Override 549 public void actionPerformed(ActionEvent e) { 550 log.debug("Got an action performed for a button"); 551 for (TagCarItem thisRow : tagList) { 552 if (e.getSource().equals(thisRow.getAction1())) { 553 if (thisRow.getCurrentCar() == null) { 554 doSetTag(thisRow.getTag(), thisRow); 555 } else { 556 setCarLocation(thisRow.getCurrentCar(), thisRow); 557 } 558 return; 559 } else if (e.getSource().equals(thisRow.getAction2())) { 560 doEditCar(thisRow.getCurrentCar()); 561 return; 562 } 563 } 564 log.error("Got an actionPerformed but dont recognize source"); 565 } 566 567 static class EditTrackCellEditor extends AbstractCellEditor implements TableCellEditor { 568 569 private TableDataModel model; 570 private Component value; 571 572 public EditTrackCellEditor(TableDataModel model) { 573 super(); 574 this.model = model; 575 } 576 577 @Override 578 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 579 if (model == null) { 580 log.error("was not called through the correct constructor - model is null"); 581 return null; 582 } 583 int tempCol = column; 584 if (model.parentPane == null) { 585 log.error("parent pane pointer is missing"); 586 return null; 587 } 588 if (!model.parentPane.getShowTimestamps()) { 589 tempCol = column + 1; // if timestamps are not visible, the column counter will be off 590 } 591 if (tempCol == TableDataModel.TRACK_COLUMN) { 592 this.value = model.tagList.get(row).getTrackCombo(); 593 return model.getTrackRowEditor(table, value, isSelected, row, column); 594 } else if (tempCol == TableDataModel.LOCATION_COLUMN) { 595 this.value = model.tagList.get(row).getLocationCombo(); 596 return model.getLocationRowEditor(table, value, isSelected, row, column); 597 } else if (tempCol == TableDataModel.ACTION1_COLUMN) { 598 this.value = model.tagList.get(row).getAction1(); 599 return model.tagList.get(row).getAction1(); 600 } else if (tempCol == TableDataModel.ACTION2_COLUMN) { 601 this.value = model.tagList.get(row).getAction2(); 602 return model.tagList.get(row).getAction2(); 603 } 604 log.error("unable to determine column (value {} - returning null for table editor", column); 605 return null; 606 } 607 608 @Override 609 public Object getCellEditorValue() { 610 return this.value; 611 } 612 613 } 614 615} 616