001package jmri.jmrit.operations.trains.gui;
002
003import java.awt.*;
004import java.beans.PropertyChangeEvent;
005import java.beans.PropertyChangeListener;
006import java.util.Hashtable;
007import java.util.List;
008
009import javax.swing.*;
010import javax.swing.table.*;
011
012import jmri.InstanceManager;
013import jmri.jmrit.beantable.EnablingCheckboxRenderer;
014import jmri.jmrit.operations.OperationsTableModel;
015import jmri.jmrit.operations.routes.Route;
016import jmri.jmrit.operations.setup.Control;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.jmrit.operations.trains.Train;
019import jmri.jmrit.operations.trains.TrainManager;
020import jmri.util.swing.JmriJOptionPane;
021import jmri.util.swing.XTableColumnModel;
022import jmri.util.table.ButtonEditor;
023import jmri.util.table.ButtonRenderer;
024
025/**
026 * Table Model for edit of trains used by operations
027 *
028 * @author Daniel Boudreau Copyright (C) 2008, 2012, 2026
029 */
030public class TrainsTableModel extends OperationsTableModel implements PropertyChangeListener {
031
032    TrainManager trainManager = InstanceManager.getDefault(TrainManager.class); // There is only one manager
033    volatile List<Train> sysList = trainManager.getTrainsByTimeList();
034    TrainsTableFrame _frame = null;
035
036    // Defines the columns
037    private static final int ID_COLUMN = 0;
038    private static final int TIME_COLUMN = ID_COLUMN + 1;
039    private static final int DONE_COLUMN = TIME_COLUMN + 1;
040    private static final int BUILDBOX_COLUMN = DONE_COLUMN + 1;
041    private static final int BUILD_COLUMN = BUILDBOX_COLUMN + 1;
042    private static final int NAME_COLUMN = BUILD_COLUMN + 1;
043    private static final int DESCRIPTION_COLUMN = NAME_COLUMN + 1;
044    private static final int BUILT_COLUMN = DESCRIPTION_COLUMN + 1;
045    private static final int CAR_ROAD_COLUMN = BUILT_COLUMN + 1;
046    private static final int CABOOSE_ROAD_COLUMN = CAR_ROAD_COLUMN + 1;
047    private static final int LOCO_ROAD_COLUMN = CABOOSE_ROAD_COLUMN + 1;
048    private static final int LOAD_COLUMN = LOCO_ROAD_COLUMN + 1;
049    private static final int OWNER_COLUMN = LOAD_COLUMN + 1;
050    private static final int ROUTE_COLUMN = OWNER_COLUMN + 1;
051    private static final int DEPARTS_COLUMN = ROUTE_COLUMN + 1;
052    private static final int TERMINATES_COLUMN = DEPARTS_COLUMN + 1;
053    private static final int CURRENT_COLUMN = TERMINATES_COLUMN + 1;
054    private static final int CARS_COLUMN = CURRENT_COLUMN + 1;
055    private static final int STATUS_COLUMN = CARS_COLUMN + 1;
056    private static final int ACTION_COLUMN = STATUS_COLUMN + 1;
057    private static final int EDIT_COLUMN = ACTION_COLUMN + 1;
058
059    private static final int HIGHESTCOLUMN = EDIT_COLUMN + 1;
060
061    public TrainsTableModel() {
062        super();
063        trainManager.addPropertyChangeListener(this);
064        Setup.getDefault().addPropertyChangeListener(this);
065        updateList();
066    }
067
068    public final int SORTBYTIME = 2;
069    public final int SORTBYID = 7;
070
071    private int _sort = SORTBYTIME;
072
073    public void setSort(int sort) {
074        _sort = sort;
075        updateList();
076        updateColumnVisible();
077    }
078
079    private boolean _showAll = true;
080
081    public void setShowAll(boolean showAll) {
082        _showAll = showAll;
083        updateList();
084        fireTableDataChanged();
085    }
086
087    public boolean isShowAll() {
088        return _showAll;
089    }
090
091    private void updateList() {
092        // first, remove listeners from the individual objects
093        removePropertyChangeTrains();
094
095        List<Train> tempList;
096        if (_sort == SORTBYID) {
097            tempList = trainManager.getTrainsByIdList();
098        } else {
099            tempList = trainManager.getTrainsByTimeList();
100        }
101
102        if (!isShowAll()) {
103            // filter out trains not checked
104            for (int i = tempList.size() - 1; i >= 0; i--) {
105                if (!tempList.get(i).isBuildEnabled()) {
106                    tempList.remove(i);
107                }
108            }
109        }
110        sysList = tempList;
111
112        // and add listeners back in
113        addPropertyChangeTrains();
114    }
115
116    private Train getTrainByRow(int row) {
117        return sysList.get(row);
118    }
119
120    void initTable(JTable table, TrainsTableFrame frame) {
121        _table = table;
122        _frame = frame;
123        // allow row color to be controlled
124        table.setDefaultRenderer(Object.class, new MyTableCellRenderer());
125        table.setDefaultRenderer(Integer.class, new MyTableCellRenderer());
126        initTable();
127    }
128
129    // Train frame table column widths, starts with id column and ends with edit
130    private final int[] _tableColumnWidths =
131            {50, 50, 50, 50, 72, 100, 140, 50, 50, 50, 50, 50, 50, 120, 120, 120, 120, 50, 120, 90,
132                    70};
133
134    void initTable() {
135        // Use XTableColumnModel so we can control which columns are visible
136        XTableColumnModel tcm = new XTableColumnModel();
137        _table.setColumnModel(tcm);
138        _table.createDefaultColumnsFromModel();
139
140        // Install the button handlers
141        ButtonRenderer buttonRenderer = new ButtonRenderer();
142        ButtonRenderer buttonRenderer2 = new ButtonRenderer(); // for tool tips
143        TableCellEditor buttonEditor = new ButtonEditor(new javax.swing.JButton());
144        tcm.getColumn(EDIT_COLUMN).setCellRenderer(buttonRenderer);
145        tcm.getColumn(EDIT_COLUMN).setCellEditor(buttonEditor);
146        tcm.getColumn(ACTION_COLUMN).setCellRenderer(buttonRenderer);
147        tcm.getColumn(ACTION_COLUMN).setCellEditor(buttonEditor);
148        tcm.getColumn(BUILD_COLUMN).setCellRenderer(buttonRenderer2);
149        tcm.getColumn(BUILD_COLUMN).setCellEditor(buttonEditor);
150        _table.setDefaultRenderer(Boolean.class, new EnablingCheckboxRenderer());
151        
152        // for tool tips
153        DefaultTableCellRenderer defaultRenderer = new DefaultTableCellRenderer();
154        tcm.getColumn(TIME_COLUMN).setCellRenderer(defaultRenderer);
155        tcm.getColumn(DONE_COLUMN).setCellRenderer(defaultRenderer);
156
157        // set column preferred widths
158        for (int i = 0; i < tcm.getColumnCount(); i++) {
159            tcm.getColumn(i).setPreferredWidth(_tableColumnWidths[i]);
160        }
161        _frame.loadTableDetails(_table);
162        
163        // don't allow sorting for Time or Done columns
164        TableRowSorter<? extends TableModel> sorter = (TableRowSorter<? extends TableModel>) _table.getRowSorter();
165        sorter.setSortable(TIME_COLUMN, false);
166        sorter.setSortable(DONE_COLUMN, false);
167
168        // turn off column
169        updateColumnVisible();
170    }
171
172    private void updateColumnVisible() {
173        XTableColumnModel tcm = (XTableColumnModel) _table.getColumnModel();
174        tcm.setColumnVisible(tcm.getColumnByModelIndex(ID_COLUMN), _sort == SORTBYID);
175        tcm.setColumnVisible(tcm.getColumnByModelIndex(TIME_COLUMN), _sort == SORTBYTIME);
176        tcm.setColumnVisible(tcm.getColumnByModelIndex(DONE_COLUMN), Setup.isBuildOnTime());
177        tcm.setColumnVisible(tcm.getColumnByModelIndex(BUILT_COLUMN), trainManager.isBuiltRestricted());
178        tcm.setColumnVisible(tcm.getColumnByModelIndex(CAR_ROAD_COLUMN), trainManager.isCarRoadRestricted());
179        tcm.setColumnVisible(tcm.getColumnByModelIndex(CABOOSE_ROAD_COLUMN), trainManager.isCabooseRoadRestricted());
180        tcm.setColumnVisible(tcm.getColumnByModelIndex(LOCO_ROAD_COLUMN), trainManager.isLocoRoadRestricted());
181        tcm.setColumnVisible(tcm.getColumnByModelIndex(LOAD_COLUMN), trainManager.isLoadRestricted());
182        tcm.setColumnVisible(tcm.getColumnByModelIndex(OWNER_COLUMN), trainManager.isOwnerRestricted());
183    }
184
185    @Override
186    public int getRowCount() {
187        return sysList.size();
188    }
189
190    @Override
191    public int getColumnCount() {
192        return HIGHESTCOLUMN;
193    }
194
195    public static final String IDCOLUMNNAME = Bundle.getMessage("Id");
196    public static final String TIMECOLUMNNAME = Bundle.getMessage("Time");
197    public static final String DONECOLUMNNAME = Bundle.getMessage("Done");
198    public static final String BUILDBOXCOLUMNNAME = Bundle.getMessage("Build");
199    public static final String BUILDCOLUMNNAME = Bundle.getMessage("Function");
200    public static final String NAMECOLUMNNAME = Bundle.getMessage("Name");
201    public static final String DESCRIPTIONCOLUMNNAME = Bundle.getMessage("Description");
202    public static final String ROUTECOLUMNNAME = Bundle.getMessage("Route");
203    public static final String DEPARTSCOLUMNNAME = Bundle.getMessage("Departs");
204    public static final String CURRENTCOLUMNNAME = Bundle.getMessage("Current");
205    public static final String TERMINATESCOLUMNNAME = Bundle.getMessage("Terminates");
206    public static final String STATUSCOLUMNNAME = Bundle.getMessage("Status");
207    public static final String ACTIONCOLUMNNAME = Bundle.getMessage("Action");
208    public static final String EDITCOLUMNNAME = Bundle.getMessage("ButtonEdit");
209
210    @Override
211    public String getColumnName(int col) {
212        switch (col) {
213            case ID_COLUMN:
214                return IDCOLUMNNAME;
215            case TIME_COLUMN:
216                return TIMECOLUMNNAME;
217            case DONE_COLUMN:
218                return DONECOLUMNNAME;
219            case BUILDBOX_COLUMN:
220                return BUILDBOXCOLUMNNAME;
221            case BUILD_COLUMN:
222                return BUILDCOLUMNNAME;
223            case NAME_COLUMN:
224                return NAMECOLUMNNAME;
225            case DESCRIPTION_COLUMN:
226                return DESCRIPTIONCOLUMNNAME;
227            case BUILT_COLUMN:
228                return Bundle.getMessage("Built");
229            case CAR_ROAD_COLUMN:
230                return Bundle.getMessage("RoadsCar");
231            case CABOOSE_ROAD_COLUMN:
232                return Bundle.getMessage("RoadsCaboose");
233            case LOCO_ROAD_COLUMN:
234                return Bundle.getMessage("RoadsLoco");
235            case LOAD_COLUMN:
236                return Bundle.getMessage("Load");
237            case OWNER_COLUMN:
238                return Bundle.getMessage("Owner");
239            case ROUTE_COLUMN:
240                return ROUTECOLUMNNAME;
241            case DEPARTS_COLUMN:
242                return DEPARTSCOLUMNNAME;
243            case CURRENT_COLUMN:
244                return CURRENTCOLUMNNAME;
245            case TERMINATES_COLUMN:
246                return TERMINATESCOLUMNNAME;
247            case CARS_COLUMN:
248                return Bundle.getMessage("Cars");
249            case STATUS_COLUMN:
250                return STATUSCOLUMNNAME;
251            case ACTION_COLUMN:
252                return ACTIONCOLUMNNAME;
253            case EDIT_COLUMN:
254                return EDITCOLUMNNAME;
255            default:
256                return "unknown"; // NOI18N
257        }
258    }
259
260    @Override
261    public Class<?> getColumnClass(int col) {
262        switch (col) {
263            case BUILDBOX_COLUMN:
264                return Boolean.class;
265            case ID_COLUMN:
266            case CARS_COLUMN:
267                return Integer.class;
268            case TIME_COLUMN:
269            case DONE_COLUMN:
270            case NAME_COLUMN:
271            case DESCRIPTION_COLUMN:
272            case BUILT_COLUMN:
273            case CAR_ROAD_COLUMN:
274            case CABOOSE_ROAD_COLUMN:
275            case LOCO_ROAD_COLUMN:
276            case LOAD_COLUMN:
277            case OWNER_COLUMN:
278            case ROUTE_COLUMN:
279            case DEPARTS_COLUMN:
280            case CURRENT_COLUMN:
281            case TERMINATES_COLUMN:
282            case STATUS_COLUMN:
283                return String.class;
284            case BUILD_COLUMN:
285            case ACTION_COLUMN:
286            case EDIT_COLUMN:
287                return JButton.class;
288            default:
289                return null;
290        }
291    }
292
293    @Override
294    public boolean isCellEditable(int row, int col) {
295        switch (col) {
296            case BUILD_COLUMN:
297            case BUILDBOX_COLUMN:
298            case ACTION_COLUMN:
299            case EDIT_COLUMN:
300                return true;
301            default:
302                return false;
303        }
304    }
305
306    @Override
307    public Object getValueAt(int row, int col) {
308        if (row >= getRowCount()) {
309            return "ERROR row " + row; // NOI18N
310        }
311        Train train = getTrainByRow(row);
312        if (train == null) {
313            return "ERROR train unknown " + row; // NOI18N
314        }
315        switch (col) {
316            case ID_COLUMN:
317                return Integer.parseInt(train.getId());
318            case TIME_COLUMN:
319                setToolTip(Bundle.getMessage("TimeTip"), col);
320                return train.getDepartureTime();
321            case DONE_COLUMN:
322                setToolTip(Bundle.getMessage("TimeTip"), col);
323                return getDoneTime(train);
324            case NAME_COLUMN:
325                return train.getIconName();
326            case DESCRIPTION_COLUMN:
327                return train.getDescription();
328            case BUILDBOX_COLUMN:
329                return Boolean.valueOf(train.isBuildEnabled());
330            case BUILT_COLUMN:
331                return getBuiltString(train);
332            case CAR_ROAD_COLUMN:
333                return getModifiedString(train.getCarRoadNames().length,
334                        train.getCarRoadOption().equals(Train.ALL_ROADS),
335                        train.getCarRoadOption().equals(Train.INCLUDE_ROADS));
336            case CABOOSE_ROAD_COLUMN:
337                return getModifiedString(train.getCabooseRoadNames().length,
338                        train.getCabooseRoadOption().equals(Train.ALL_ROADS),
339                        train.getCabooseRoadOption().equals(Train.INCLUDE_ROADS));
340            case LOCO_ROAD_COLUMN:
341                return getModifiedString(train.getLocoRoadNames().length,
342                        train.getLocoRoadOption().equals(Train.ALL_ROADS),
343                        train.getLocoRoadOption().equals(Train.INCLUDE_ROADS));
344            case LOAD_COLUMN:
345                return getModifiedString(train.getLoadNames().length, train.getLoadOption().equals(Train.ALL_LOADS),
346                        train.getLoadOption().equals(Train.INCLUDE_LOADS));
347            case OWNER_COLUMN:
348                return getModifiedString(train.getOwnerNames().length, train.getOwnerOption().equals(Train.ALL_OWNERS),
349                        train.getOwnerOption().equals(Train.INCLUDE_OWNERS));
350            case ROUTE_COLUMN:
351                return train.getTrainRouteName();
352            case DEPARTS_COLUMN: {
353                if (train.getDepartureTrack() == null) {
354                    return train.getTrainDepartsName();
355                } else {
356                    return train.getTrainDepartsName() + " (" + train.getDepartureTrack().getName() + ")";
357                }
358            }
359            case CURRENT_COLUMN:
360                return train.getCurrentLocationName();
361            case TERMINATES_COLUMN: {
362                if (train.getTerminationTrack() == null) {
363                    return train.getTrainTerminatesName();
364                } else {
365                    return train.getTrainTerminatesName() + " (" + train.getTerminationTrack().getName() + ")";
366                }
367            }
368            case CARS_COLUMN:
369                return train.getNumberCarsInTrain();
370            case STATUS_COLUMN:
371                return train.getStatus();
372            case BUILD_COLUMN: {
373                if (train.isBuilt()) {
374                    if (Setup.isGenerateCsvManifestEnabled() && trainManager.isOpenFileEnabled()) {
375                        setToolTip(Bundle.getMessage("OpenTrainTip", train.getName()), col);
376                        return Bundle.getMessage("OpenFile");
377                    }
378                    if (Setup.isGenerateCsvManifestEnabled() && trainManager.isRunFileEnabled()) {
379                        setToolTip(Bundle.getMessage("RunTrainTip", train.getName()), col);
380                        return Bundle.getMessage("RunFile");
381                    }
382                    setToolTip(Bundle.getMessage("PrintTrainTip"), col);
383                    if (trainManager.isPrintPreviewEnabled()) {
384                        return Bundle.getMessage("Preview");
385                    } else if (train.isPrinted()) {
386                        return Bundle.getMessage("Printed");
387                    } else {
388                        return Bundle.getMessage("Print");
389                    }
390                }
391                setToolTip(Bundle.getMessage("BuildTrainTip", train.getName()), col);
392                return Bundle.getMessage("Build");
393            }
394            case ACTION_COLUMN: {
395                if (train.isBuildFailed()) {
396                    return Bundle.getMessage("Report");
397                }
398                if (train.getCurrentRouteLocation() == train.getTrainTerminatesRouteLocation() &&
399                        trainManager.getTrainsFrameTrainAction().equals(TrainsTableFrame.MOVE)) {
400                    return Bundle.getMessage("Terminate");
401                }
402                return trainManager.getTrainsFrameTrainAction();
403            }
404            case EDIT_COLUMN:
405                return Bundle.getMessage("ButtonEdit");
406            default:
407                return "unknown " + col; // NOI18N
408        }
409    }
410
411    private String getBuiltString(Train train) {
412        if (!train.getBuiltStartYear().equals(Train.NONE) && train.getBuiltEndYear().equals(Train.NONE)) {
413            return "A " + train.getBuiltStartYear();
414        }
415        if (train.getBuiltStartYear().equals(Train.NONE) && !train.getBuiltEndYear().equals(Train.NONE)) {
416            return "B " + train.getBuiltEndYear();
417        }
418        if (!train.getBuiltStartYear().equals(Train.NONE) && !train.getBuiltEndYear().equals(Train.NONE)) {
419            return "R " + train.getBuiltStartYear() + ":" + train.getBuiltEndYear();
420        }
421        return "";
422    }
423
424    private String getModifiedString(int number, boolean all, boolean accept) {
425        if (all) {
426            return "";
427        }
428        if (accept) {
429            return "A " + Integer.toString(number); // NOI18N
430        }
431        return "E " + Integer.toString(number); // NOI18N
432    }
433    
434    private String getDoneTime(Train train) {
435        return train.getExpectedDepartureTime(train.getTrainTerminatesRouteLocation(), true);
436    }
437
438    @Override
439    public void setValueAt(Object value, int row, int col) {
440        switch (col) {
441            case EDIT_COLUMN:
442                editTrain(row);
443                break;
444            case BUILD_COLUMN:
445                buildTrain(row);
446                break;
447            case ACTION_COLUMN:
448                actionTrain(row);
449                break;
450            case BUILDBOX_COLUMN: {
451                Train train = getTrainByRow(row);
452                train.setBuildEnabled(((Boolean) value).booleanValue());
453                break;
454            }
455            default:
456                break;
457        }
458    }
459
460    public Color getRowColor(int row) {
461        Train train = getTrainByRow(row);
462        return train.getTableRowColor();
463    }
464
465    TrainEditFrame tef = null;
466
467    private void editTrain(int row) {
468        if (tef != null) {
469            tef.dispose();
470        }
471        // use invokeLater so new window appears on top
472        SwingUtilities.invokeLater(() -> {
473            Train train = getTrainByRow(row);
474            log.debug("Edit train ({})", train.getName());
475            tef = new TrainEditFrame(train);
476        });
477    }
478
479    Thread build;
480
481    private void buildTrain(int row) {
482        final Train train = getTrainByRow(row);
483        if (!train.isBuilt()) {
484            // only one train build at a time
485            if (build != null && build.isAlive()) {
486                return;
487            }
488            // use a thread to allow table updates during build
489            build = jmri.util.ThreadingUtil.newThread(new Runnable() {
490                @Override
491                public void run() {
492                    train.build();
493                }
494            });
495            build.setName("Build Train (" + train.getName() + ")"); // NOI18N
496            build.start();
497            // print build report, print manifest, run or open file
498        } else {
499            if (trainManager.isBuildReportEnabled()) {
500                train.printBuildReport();
501            }
502            if (Setup.isGenerateCsvManifestEnabled() && trainManager.isOpenFileEnabled()) {
503                train.openFile();
504            } else if (Setup.isGenerateCsvManifestEnabled() && trainManager.isRunFileEnabled()) {
505                train.runFile();
506            } else {
507                if (!train.printManifestIfBuilt()) {
508                    log.debug("Manifest file for train ({}) not found", train.getName());
509                    int result = JmriJOptionPane.showConfirmDialog(null,
510                            Bundle.getMessage("TrainManifestFileMissing",
511                                    train.getName()),
512                            Bundle.getMessage("TrainManifestFileError"), JmriJOptionPane.YES_NO_OPTION);
513                    if (result == JmriJOptionPane.YES_OPTION) {
514                        train.setModified(true);
515                        if (!train.printManifestIfBuilt()) {
516                            log.error("Unable to create manifest for train ({})", train.getName());
517                        }
518                    }
519                }
520            }
521        }
522    }
523
524    // one of five buttons: Report, Move, Reset, Conductor or Terminate
525    private void actionTrain(int row) {
526        // no actions while a train is being built
527        if (build != null && build.isAlive()) {
528            return;
529        }
530        Train train = getTrainByRow(row);
531        // move button becomes report if failure
532        if (train.isBuildFailed()) {
533            train.printBuildReport();
534        } else if (trainManager.getTrainsFrameTrainAction().equals(TrainsTableFrame.RESET)) {
535            log.debug("Reset train ({})", train.getName());
536            Train t = trainManager.getTrainBuiltAfter(train);
537            // check to see if departure track was reused
538            if (train.checkDepartureTrack()) {
539                log.debug("Train is departing staging that already has inbound cars");
540                JmriJOptionPane.showMessageDialog(null,
541                        Bundle.getMessage("StagingTrackUsed",
542                                train.getDepartureTrack().getName()),
543                        Bundle.getMessage("CanNotResetTrain"), JmriJOptionPane.WARNING_MESSAGE);
544            } else if (Setup.isBuildOnTime() && t != null) {
545                JmriJOptionPane.showMessageDialog(null,
546                        Bundle.getMessage("TrainAfterBuilt", t, train), Bundle.getMessage("CanNotResetTrain"),
547                        JmriJOptionPane.WARNING_MESSAGE);
548            } else if (!train.reset()) {
549                JmriJOptionPane.showMessageDialog(null,
550                        Bundle.getMessage("TrainIsInRoute",
551                                train.getTrainTerminatesName()),
552                        Bundle.getMessage("CanNotResetTrain"), JmriJOptionPane.ERROR_MESSAGE);
553            }
554        } else if (!train.isBuilt()) {
555            int reply = JmriJOptionPane.showOptionDialog(null,
556                    Bundle.getMessage("TrainNeedsBuild", train.getName()),
557                    Bundle.getMessage("CanNotPerformAction"), JmriJOptionPane.NO_OPTION,
558                    JmriJOptionPane.INFORMATION_MESSAGE, null,
559                    new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("Build")}, null);
560            if (reply == 1) {
561                train.build();
562            }
563        } else if (train.isBuilt() && trainManager.getTrainsFrameTrainAction().equals(TrainsTableFrame.MOVE)) {
564            log.debug("Move train ({})", train.getName());
565            train.move();
566        } else if (train.isBuilt() && trainManager.getTrainsFrameTrainAction().equals(TrainsTableFrame.TERMINATE)) {
567            log.debug("Terminate train ({})", train.getName());
568            int status = JmriJOptionPane.showConfirmDialog(null,
569                    Bundle.getMessage("TerminateTrain",
570                            train.getName(), train.getDescription()),
571                    Bundle.getMessage("DoYouWantToTermiate", train.getName()),
572                    JmriJOptionPane.YES_NO_OPTION);
573            if (status == JmriJOptionPane.YES_OPTION) {
574                train.terminate();
575            }
576        } else if (train.isBuilt() && trainManager.getTrainsFrameTrainAction().equals(TrainsTableFrame.CONDUCTOR)) {
577            log.debug("Enable conductor for train ({})", train.getName());
578            launchConductor(train);
579        }
580    }
581
582    private static Hashtable<String, TrainConductorFrame> _trainConductorHashTable = new Hashtable<>();
583
584    private void launchConductor(Train train) {
585        // use invokeLater so new window appears on top
586        SwingUtilities.invokeLater(() -> {
587            TrainConductorFrame f = _trainConductorHashTable.get(train.getId());
588            // create a copy train frame
589            if (f == null || !f.isVisible()) {
590                f = new TrainConductorFrame(train);
591                _trainConductorHashTable.put(train.getId(), f);
592            } else {
593                f.setExtendedState(Frame.NORMAL);
594            }
595            f.setVisible(true); // this also brings the frame into focus
596        });
597    }
598
599    @Override
600    public void propertyChange(PropertyChangeEvent e) {
601        if (Control.SHOW_PROPERTY) {
602            log.debug("Property change {} old: {} new: {}", e.getPropertyName(), e.getOldValue(), e.getNewValue()); // NOI18N
603        }
604        if (e.getPropertyName().equals(Train.BUILT_YEAR_CHANGED_PROPERTY) ||
605                e.getPropertyName().equals(Train.ROADS_CHANGED_PROPERTY) ||
606                e.getPropertyName().equals(Train.LOADS_CHANGED_PROPERTY) ||
607                e.getPropertyName().equals(Train.OWNERS_CHANGED_PROPERTY)) {
608            updateColumnVisible();
609        }
610        if (e.getPropertyName().equals(TrainManager.LISTLENGTH_CHANGED_PROPERTY) ||
611                e.getPropertyName().equals(TrainManager.PRINTPREVIEW_CHANGED_PROPERTY) ||
612                e.getPropertyName().equals(TrainManager.OPEN_FILE_CHANGED_PROPERTY) ||
613                e.getPropertyName().equals(TrainManager.RUN_FILE_CHANGED_PROPERTY) ||
614                e.getPropertyName().equals(Setup.MANIFEST_CSV_PROPERTY_CHANGE) ||
615                e.getPropertyName().equals(TrainManager.TRAIN_ACTION_CHANGED_PROPERTY) ||
616                e.getPropertyName().equals(Train.DEPARTURETIME_CHANGED_PROPERTY) ||
617                (e.getPropertyName().equals(Train.BUILD_CHANGED_PROPERTY) && !isShowAll())) {
618            SwingUtilities.invokeLater(() -> {
619                updateList();
620                fireTableDataChanged();
621            });
622        } else if (e.getSource().getClass().equals(Train.class) &&
623                !e.getPropertyName().equals(Route.ROUTE_STATUS_CHANGED_PROPERTY)) {
624            Train train = ((Train) e.getSource());
625            SwingUtilities.invokeLater(() -> {
626                int row = sysList.indexOf(train);
627                if (row >= 0 && _table != null) {
628                    fireTableRowsUpdated(row, row);
629                    int viewRow = _table.convertRowIndexToView(row);
630                    log.debug("Scroll table to row: {}, train: {}, property: {}", viewRow, train.getName(),
631                            e.getPropertyName());
632                    _table.scrollRectToVisible(_table.getCellRect(viewRow, 0, true));
633                }
634            });
635        }
636    }
637
638    private void removePropertyChangeTrains() {
639        for (Train train : trainManager.getList()) {
640            train.removePropertyChangeListener(this);
641        }
642    }
643
644    private void addPropertyChangeTrains() {
645        for (Train train : trainManager.getList()) {
646            train.addPropertyChangeListener(this);
647        }
648    }
649
650    public void dispose() {
651        if (tef != null) {
652            tef.dispose();
653        }
654        trainManager.removePropertyChangeListener(this);
655        Setup.getDefault().removePropertyChangeListener(this);
656        removePropertyChangeTrains();
657    }
658
659    class MyTableCellRenderer extends DefaultTableCellRenderer {
660
661        @Override
662        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
663                int row, int column) {
664            Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
665            if (!isSelected) {
666                int modelRow = table.convertRowIndexToModel(row);
667                // log.debug("View row: {} Column: {} Model row: {}", row, column, modelRow);
668                Color background = getRowColor(modelRow);
669                component.setBackground(background);
670                component.setForeground(getForegroundColor(background));
671            }
672            return component;
673        }
674
675        Color[] darkColors = {Color.BLACK, Color.BLUE, Color.GRAY, Color.RED, Color.MAGENTA};
676
677        /**
678         * Dark colors need white lettering
679         */
680        private Color getForegroundColor(Color background) {
681            if (background == null) {
682                return null;
683            }
684            for (Color color : darkColors) {
685                if (background == color) {
686                    return Color.WHITE;
687                }
688            }
689            return Color.BLACK; // all others get black lettering
690        }
691    }
692
693    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrainsTableModel.class);
694}