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.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