001package jmri.jmrit.operations.trains;
002
003import java.beans.PropertyChangeListener;
004import java.io.File;
005import java.io.PrintWriter;
006import java.util.*;
007
008import javax.swing.JComboBox;
009
010import org.jdom2.Attribute;
011import org.jdom2.Element;
012
013import jmri.*;
014import jmri.beans.PropertyChangeSupport;
015import jmri.jmrit.operations.OperationsPanel;
016import jmri.jmrit.operations.locations.Location;
017import jmri.jmrit.operations.rollingstock.cars.Car;
018import jmri.jmrit.operations.rollingstock.cars.CarLoad;
019import jmri.jmrit.operations.routes.Route;
020import jmri.jmrit.operations.routes.RouteLocation;
021import jmri.jmrit.operations.setup.OperationsSetupXml;
022import jmri.jmrit.operations.setup.Setup;
023import jmri.jmrit.operations.trains.excel.TrainCustomManifest;
024import jmri.jmrit.operations.trains.excel.TrainCustomSwitchList;
025import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
026import jmri.script.JmriScriptEngineManager;
027import jmri.util.ColorUtil;
028import jmri.util.swing.JmriJOptionPane;
029
030/**
031 * Manages trains.
032 *
033 * @author Bob Jacobsen Copyright (C) 2003
034 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013,
035 *         2014
036 */
037public class TrainManager extends PropertyChangeSupport
038        implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize, PropertyChangeListener {
039
040    static final String NONE = "";
041
042    // Train frame attributes
043    private String _trainAction = TrainsTableFrame.MOVE; // Trains frame table button action
044    private boolean _buildMessages = true; // when true, show build messages
045    private boolean _buildReport = false; // when true, print/preview build reports
046    private boolean _printPreview = false; // when true, preview train manifest
047    private boolean _openFile = false; // when true, open CSV file manifest
048    private boolean _runFile = false; // when true, run CSV file manifest
049
050    // Conductor attributes
051    private boolean _showLocationHyphenName = false;
052
053    // Trains window row colors
054    private boolean _rowColorManual = true; // when true train colors are manually assigned
055    private String _rowColorBuilt = NONE; // row color when train is built
056    private String _rowColorBuildFailed = NONE; // row color when train build failed
057    private String _rowColorTrainEnRoute = NONE; // row color when train is en route
058    private String _rowColorTerminated = NONE; // row color when train is terminated
059    private String _rowColorReset = NONE; // row color when train is reset
060
061    // Scripts
062    protected List<String> _startUpScripts = new ArrayList<>(); // list of script pathnames to run at start up
063    protected List<String> _shutDownScripts = new ArrayList<>(); // list of script pathnames to run at shut down
064
065    // property changes
066    public static final String LISTLENGTH_CHANGED_PROPERTY = "TrainsListLength"; // NOI18N
067    public static final String PRINTPREVIEW_CHANGED_PROPERTY = "TrainsPrintPreview"; // NOI18N
068    public static final String OPEN_FILE_CHANGED_PROPERTY = "TrainsOpenFile"; // NOI18N
069    public static final String RUN_FILE_CHANGED_PROPERTY = "TrainsRunFile"; // NOI18N
070    public static final String TRAIN_ACTION_CHANGED_PROPERTY = "TrainsAction"; // NOI18N
071    public static final String ROW_COLOR_NAME_CHANGED_PROPERTY = "TrainsRowColorChange"; // NOI18N
072    public static final String TRAINS_BUILT_CHANGED_PROPERTY = "TrainsBuiltChange"; // NOI18N
073    public static final String TRAINS_SHOW_FULL_NAME_PROPERTY = "TrainsShowFullName"; // NOI18N
074    public static final String TRAINS_SAVED_PROPERTY = "TrainsSaved"; // NOI18N
075
076    public TrainManager() {
077    }
078
079    private int _id = 0; // train ids
080
081    /**
082     * Get the number of items in the roster
083     *
084     * @return Number of trains in the roster
085     */
086    public int getNumEntries() {
087        return _trainHashTable.size();
088    }
089
090    /**
091     *
092     * @return true if build messages are enabled
093     */
094    public boolean isBuildMessagesEnabled() {
095        return _buildMessages;
096    }
097
098    public void setBuildMessagesEnabled(boolean enable) {
099        boolean old = _buildMessages;
100        _buildMessages = enable;
101        setDirtyAndFirePropertyChange("BuildMessagesEnabled", enable, old); // NOI18N
102    }
103
104    /**
105     *
106     * @return true if build reports are enabled
107     */
108    public boolean isBuildReportEnabled() {
109        return _buildReport;
110    }
111
112    public void setBuildReportEnabled(boolean enable) {
113        boolean old = _buildReport;
114        _buildReport = enable;
115        setDirtyAndFirePropertyChange("BuildReportEnabled", enable, old); // NOI18N
116    }
117
118    /**
119     *
120     * @return true if open file is enabled
121     */
122    public boolean isOpenFileEnabled() {
123        return _openFile;
124    }
125
126    public void setOpenFileEnabled(boolean enable) {
127        boolean old = _openFile;
128        _openFile = enable;
129        setDirtyAndFirePropertyChange(OPEN_FILE_CHANGED_PROPERTY, old ? "true" : "false", enable ? "true" // NOI18N
130                : "false"); // NOI18N
131    }
132
133    /**
134     *
135     * @return true if open file is enabled
136     */
137    public boolean isRunFileEnabled() {
138        return _runFile;
139    }
140
141    public void setRunFileEnabled(boolean enable) {
142        boolean old = _runFile;
143        _runFile = enable;
144        setDirtyAndFirePropertyChange(RUN_FILE_CHANGED_PROPERTY, old ? "true" : "false", enable ? "true" // NOI18N
145                : "false"); // NOI18N
146    }
147
148    /**
149     *
150     * @return true if print preview is enabled
151     */
152    public boolean isPrintPreviewEnabled() {
153        return _printPreview;
154    }
155
156    public void setPrintPreviewEnabled(boolean enable) {
157        boolean old = _printPreview;
158        _printPreview = enable;
159        setDirtyAndFirePropertyChange(PRINTPREVIEW_CHANGED_PROPERTY, old ? "Preview" : "Print", // NOI18N
160                enable ? "Preview" : "Print"); // NOI18N
161    }
162
163    /**
164     * When true show entire location name including hyphen
165     * 
166     * @return true when showing entire location name
167     */
168    public boolean isShowLocationHyphenNameEnabled() {
169        return _showLocationHyphenName;
170    }
171
172    public void setShowLocationHyphenNameEnabled(boolean enable) {
173        boolean old = _showLocationHyphenName;
174        _showLocationHyphenName = enable;
175        setDirtyAndFirePropertyChange(TRAINS_SHOW_FULL_NAME_PROPERTY, old, enable);
176    }
177
178    public String getTrainsFrameTrainAction() {
179        return _trainAction;
180    }
181
182    public void setTrainsFrameTrainAction(String action) {
183        String old = _trainAction;
184        _trainAction = action;
185        if (!old.equals(action)) {
186            setDirtyAndFirePropertyChange(TRAIN_ACTION_CHANGED_PROPERTY, old, action);
187        }
188    }
189
190    /**
191     * Add a script to run after trains have been loaded
192     *
193     * @param pathname The script's pathname
194     */
195    public void addStartUpScript(String pathname) {
196        _startUpScripts.add(pathname);
197        setDirtyAndFirePropertyChange("addStartUpScript", pathname, null); // NOI18N
198    }
199
200    public void deleteStartUpScript(String pathname) {
201        _startUpScripts.remove(pathname);
202        setDirtyAndFirePropertyChange("deleteStartUpScript", null, pathname); // NOI18N
203    }
204
205    /**
206     * Gets a list of pathnames to run after trains have been loaded
207     *
208     * @return A list of pathnames to run after trains have been loaded
209     */
210    public List<String> getStartUpScripts() {
211        return _startUpScripts;
212    }
213
214    public void runStartUpScripts() {
215        // use thread to prevent object (Train) thread lock
216        Thread scripts = jmri.util.ThreadingUtil.newThread(new Runnable() {
217            @Override
218            public void run() {
219                for (String scriptPathName : getStartUpScripts()) {
220                    try {
221                        JmriScriptEngineManager.getDefault()
222                                .runScript(new File(jmri.util.FileUtil.getExternalFilename(scriptPathName)));
223                    } catch (Exception e) {
224                        log.error("Problem with script: {}", scriptPathName);
225                    }
226                }
227            }
228        });
229        scripts.setName("Startup Scripts"); // NOI18N
230        scripts.start();
231    }
232
233    /**
234     * Add a script to run at shutdown
235     *
236     * @param pathname The script's pathname
237     */
238    public void addShutDownScript(String pathname) {
239        _shutDownScripts.add(pathname);
240        setDirtyAndFirePropertyChange("addShutDownScript", pathname, null); // NOI18N
241    }
242
243    public void deleteShutDownScript(String pathname) {
244        _shutDownScripts.remove(pathname);
245        setDirtyAndFirePropertyChange("deleteShutDownScript", null, pathname); // NOI18N
246    }
247
248    /**
249     * Gets a list of pathnames to run at shutdown
250     *
251     * @return A list of pathnames to run at shutdown
252     */
253    public List<String> getShutDownScripts() {
254        return _shutDownScripts;
255    }
256
257    public void runShutDownScripts() {
258        for (String scriptPathName : getShutDownScripts()) {
259            try {
260                JmriScriptEngineManager.getDefault()
261                        .runScript(new File(jmri.util.FileUtil.getExternalFilename(scriptPathName)));
262            } catch (Exception e) {
263                log.error("Problem with script: {}", scriptPathName);
264            }
265        }
266    }
267
268    /**
269     * Used to determine if a train has any restrictions with regard to car
270     * built dates.
271     * 
272     * @return true if there's a restriction
273     */
274    public boolean isBuiltRestricted() {
275        for (Train train : getList()) {
276            if (!train.getBuiltStartYear().equals(Train.NONE) || !train.getBuiltEndYear().equals(Train.NONE)) {
277                return true;
278            }
279        }
280        return false;
281    }
282
283    /**
284     * Used to determine if a train has any restrictions with regard to car
285     * loads.
286     * 
287     * @return true if there's a restriction
288     */
289    public boolean isLoadRestricted() {
290        for (Train train : getList()) {
291            if (!train.getLoadOption().equals(Train.ALL_LOADS)) {
292                return true;
293            }
294        }
295        return false;
296    }
297
298    /**
299     * Used to determine if a train has any restrictions with regard to car
300     * roads.
301     * 
302     * @return true if there's a restriction
303     */
304    public boolean isCarRoadRestricted() {
305        for (Train train : getList()) {
306            if (!train.getCarRoadOption().equals(Train.ALL_ROADS)) {
307                return true;
308            }
309        }
310        return false;
311    }
312    
313    /**
314     * Used to determine if a train has any restrictions with regard to Locomotive
315     * roads.
316     * 
317     * @return true if there's a restriction
318     */
319    public boolean isLocoRoadRestricted() {
320        for (Train train : getList()) {
321            if (!train.getLocoRoadOption().equals(Train.ALL_ROADS)) {
322                return true;
323            }
324        }
325        return false;
326    }
327
328    /**
329     * Used to determine if a train has any restrictions with regard to car
330     * owners.
331     * 
332     * @return true if there's a restriction
333     */
334    public boolean isOwnerRestricted() {
335        for (Train train : getList()) {
336            if (!train.getOwnerOption().equals(Train.ALL_OWNERS)) {
337                return true;
338            }
339        }
340        return false;
341    }
342
343    public void dispose() {
344        _trainHashTable.clear();
345        _id = 0;
346    }
347
348    // stores known Train instances by id
349    private final Hashtable<String, Train> _trainHashTable = new Hashtable<>();
350
351    /**
352     * @param name The train's name.
353     * @return requested Train object or null if none exists
354     */
355    public Train getTrainByName(String name) {
356        if (!InstanceManager.getDefault(TrainManagerXml.class).isTrainFileLoaded()) {
357            log.error("TrainManager getTrainByName called before trains completely loaded!");
358        }
359        Train train;
360        Enumeration<Train> en = _trainHashTable.elements();
361        while (en.hasMoreElements()) {
362            train = en.nextElement();
363            // windows file names are case independent
364            if (train.getName().toLowerCase().equals(name.toLowerCase())) {
365                return train;
366            }
367        }
368        log.debug("Train ({}) doesn't exist", name);
369        return null;
370    }
371
372    public Train getTrainById(String id) {
373        if (!InstanceManager.getDefault(TrainManagerXml.class).isTrainFileLoaded()) {
374            log.error("TrainManager getTrainById called before trains completely loaded!");
375        }
376        return _trainHashTable.get(id);
377    }
378
379    /**
380     * Finds an existing train or creates a new train if needed. Requires train's
381     * name and creates a unique id for a new train
382     *
383     * @param name The train's name.
384     *
385     *
386     * @return new train or existing train
387     */
388    public Train newTrain(String name) {
389        Train train = getTrainByName(name);
390        if (train == null) {
391            _id++;
392            train = new Train(Integer.toString(_id), name);
393            Integer oldSize = Integer.valueOf(getNumEntries());
394            _trainHashTable.put(train.getId(), train);
395            setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize,
396                    Integer.valueOf(getNumEntries()));
397        }
398        return train;
399    }
400
401    /**
402     * Remember a NamedBean Object created outside the manager.
403     *
404     * @param train The Train to be added.
405     */
406    public void register(Train train) {
407        Integer oldSize = Integer.valueOf(getNumEntries());
408        _trainHashTable.put(train.getId(), train);
409        // find last id created
410        int id = Integer.parseInt(train.getId());
411        if (id > _id) {
412            _id = id;
413        }
414        setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, Integer.valueOf(getNumEntries()));
415    }
416
417    /**
418     * Forget a NamedBean Object created outside the manager.
419     *
420     * @param train The Train to delete.
421     */
422    public void deregister(Train train) {
423        if (train == null) {
424            return;
425        }
426        train.dispose();
427        Integer oldSize = Integer.valueOf(getNumEntries());
428        _trainHashTable.remove(train.getId());
429        setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, Integer.valueOf(getNumEntries()));
430    }
431
432    public void replaceLoad(String type, String oldLoadName, String newLoadName) {
433        for (Train train : getTrainsByIdList()) {
434            for (String loadName : train.getLoadNames()) {
435                if (loadName.equals(oldLoadName)) {
436                    train.deleteLoadName(oldLoadName);
437                    if (newLoadName != null) {
438                        train.addLoadName(newLoadName);
439                    }
440                }
441                // adjust combination car type and load name
442                String[] splitLoad = loadName.split(CarLoad.SPLIT_CHAR);
443                if (splitLoad.length > 1) {
444                    if (splitLoad[0].equals(type) && splitLoad[1].equals(oldLoadName)) {
445                        train.deleteLoadName(loadName);
446                        if (newLoadName != null) {
447                            train.addLoadName(type + CarLoad.SPLIT_CHAR + newLoadName);
448                        }
449                    }
450                }
451            }
452        }
453    }
454
455    /**
456     *
457     * @return true if there's a built train
458     */
459    public boolean isAnyTrainBuilt() {
460        for (Train train : getTrainsByIdList()) {
461            if (train.isBuilt()) {
462                return true;
463            }
464        }
465        return false;
466    }
467
468    /**
469     *
470     * @return true if there's a train being built
471     */
472    public boolean isAnyTrainBuilding() {
473        for (Train train : getTrainsByIdList()) {
474            if (train.getStatusCode() == Train.CODE_BUILDING) {
475                log.debug("Train {} is currently building", train.getName());
476                return true;
477            }
478        }
479        return false;
480    }
481
482    /**
483     * @param car         The car looking for a train.
484     * @param buildReport The optional build report for logging.
485     * @return Train that can service car from its current location to the its
486     *         destination.
487     */
488    public Train getTrainForCar(Car car, PrintWriter buildReport) {
489        return getTrainForCar(car, new ArrayList<>(), buildReport);
490    }
491
492    /**
493     * @param car           The car looking for a train.
494     * @param excludeTrains The trains not to try.
495     * @param buildReport   The optional build report for logging.
496     * @return Train that can service car from its current location to the its
497     *         destination.
498     */
499    public Train getTrainForCar(Car car, List<Train> excludeTrains, PrintWriter buildReport) {
500        addLine(buildReport, TrainCommon.BLANK_LINE);
501        addLine(buildReport, Bundle.getMessage("trainFindForCar", car.toString(), car.getLocationName(),
502                car.getTrackName(), car.getDestinationName(), car.getDestinationTrackName()));
503
504        main: for (Train train : getTrainsByNameList()) {
505            if (excludeTrains.contains(train)) {
506                continue;
507            }
508            if (Setup.isOnlyActiveTrainsEnabled() && !train.isBuildEnabled()) {
509                continue;
510            }
511            for (Train t : excludeTrains) {
512                if (t != null && train.getRoute() == t.getRoute()) {
513                    addLine(buildReport, Bundle.getMessage("trainHasSameRoute", train, t));
514                    continue main;
515                }
516            }
517            // does this train service this car?
518            if (train.isServiceable(buildReport, car)) {
519                log.debug("Found train ({}) for car ({}) location ({}, {}) destination ({}, {})", train.getName(),
520                        car.toString(), car.getLocationName(), car.getTrackName(), car.getDestinationName(),
521                        car.getDestinationTrackName()); // NOI18N
522                return train;
523            }
524        }
525        return null;
526    }
527
528    protected static final String SEVEN = Setup.BUILD_REPORT_VERY_DETAILED;
529
530    private void addLine(PrintWriter buildReport, String string) {
531        if (Setup.getRouterBuildReportLevel().equals(SEVEN)) {
532            TrainCommon.addLine(buildReport, SEVEN, string);
533        }
534    }
535
536    /**
537     * Sort by train name
538     *
539     * @return list of trains ordered by name
540     */
541    public List<Train> getTrainsByNameList() {
542        return getTrainsByList(getList(), GET_TRAIN_NAME);
543    }
544
545    /**
546     * Sort by train departure time
547     *
548     * @return list of trains ordered by departure time
549     */
550    public List<Train> getTrainsByTimeList() {
551        return getTrainsByIntList(getTrainsByNameList(), GET_TRAIN_TIME);
552    }
553
554    /**
555     * Sort by train departure location name
556     *
557     * @return list of trains ordered by departure name
558     */
559    public List<Train> getTrainsByDepartureList() {
560        return getTrainsByList(getTrainsByTimeList(), GET_TRAIN_DEPARTES_NAME);
561    }
562
563    /**
564     * Sort by train termination location name
565     *
566     * @return list of trains ordered by termination name
567     */
568    public List<Train> getTrainsByTerminatesList() {
569        return getTrainsByList(getTrainsByTimeList(), GET_TRAIN_TERMINATES_NAME);
570    }
571
572    /**
573     * Sort by train route name
574     *
575     * @return list of trains ordered by route name
576     */
577    public List<Train> getTrainsByRouteList() {
578        return getTrainsByList(getTrainsByTimeList(), GET_TRAIN_ROUTE_NAME);
579    }
580
581    /**
582     * Sort by train status
583     *
584     * @return list of trains ordered by status
585     */
586    public List<Train> getTrainsByStatusList() {
587        return getTrainsByList(getTrainsByTimeList(), GET_TRAIN_STATUS);
588    }
589
590    /**
591     * Sort by train description
592     *
593     * @return list of trains ordered by train description
594     */
595    public List<Train> getTrainsByDescriptionList() {
596        return getTrainsByList(getTrainsByTimeList(), GET_TRAIN_DESCRIPTION);
597    }
598
599    /**
600     * Sort by train id
601     *
602     * @return list of trains ordered by id
603     */
604    public List<Train> getTrainsByIdList() {
605        return getTrainsByIntList(getList(), GET_TRAIN_ID);
606    }
607
608    private List<Train> getTrainsByList(List<Train> sortList, int attribute) {
609        List<Train> out = new ArrayList<>();
610        for (Train train : sortList) {
611            String trainAttribute = (String) getTrainAttribute(train, attribute);
612            for (int j = 0; j < out.size(); j++) {
613                if (trainAttribute.compareToIgnoreCase((String) getTrainAttribute(out.get(j), attribute)) < 0) {
614                    out.add(j, train);
615                    break;
616                }
617            }
618            if (!out.contains(train)) {
619                out.add(train);
620            }
621        }
622        return out;
623    }
624
625    private List<Train> getTrainsByIntList(List<Train> sortList, int attribute) {
626        List<Train> out = new ArrayList<>();
627        for (Train train : sortList) {
628            int trainAttribute = (Integer) getTrainAttribute(train, attribute);
629            for (int j = 0; j < out.size(); j++) {
630                if (trainAttribute < (Integer) getTrainAttribute(out.get(j), attribute)) {
631                    out.add(j, train);
632                    break;
633                }
634            }
635            if (!out.contains(train)) {
636                out.add(train);
637            }
638        }
639        return out;
640    }
641
642    // the various sort options for trains
643    private static final int GET_TRAIN_DEPARTES_NAME = 0;
644    private static final int GET_TRAIN_NAME = 1;
645    private static final int GET_TRAIN_ROUTE_NAME = 2;
646    private static final int GET_TRAIN_TERMINATES_NAME = 3;
647    private static final int GET_TRAIN_TIME = 4;
648    private static final int GET_TRAIN_STATUS = 5;
649    private static final int GET_TRAIN_ID = 6;
650    private static final int GET_TRAIN_DESCRIPTION = 7;
651
652    private Object getTrainAttribute(Train train, int attribute) {
653        switch (attribute) {
654            case GET_TRAIN_DEPARTES_NAME:
655                return train.getTrainDepartsName();
656            case GET_TRAIN_NAME:
657                return train.getName();
658            case GET_TRAIN_ROUTE_NAME:
659                return train.getTrainRouteName();
660            case GET_TRAIN_TERMINATES_NAME:
661                return train.getTrainTerminatesName();
662            case GET_TRAIN_TIME:
663                return train.getDepartTimeMinutes();
664            case GET_TRAIN_STATUS:
665                return train.getStatus();
666            case GET_TRAIN_ID:
667                return Integer.parseInt(train.getId());
668            case GET_TRAIN_DESCRIPTION:
669                return train.getDescription();
670            default:
671                return "unknown"; // NOI18N
672        }
673    }
674
675    private List<Train> getList() {
676        if (!InstanceManager.getDefault(TrainManagerXml.class).isTrainFileLoaded()) {
677            log.error("TrainManager getList called before trains completely loaded!");
678        }
679        List<Train> out = new ArrayList<>();
680        Enumeration<Train> en = _trainHashTable.elements();
681        while (en.hasMoreElements()) {
682            out.add(en.nextElement());
683        }
684        return out;
685    }
686
687    public JComboBox<Train> getTrainComboBox() {
688        JComboBox<Train> box = new JComboBox<>();
689        updateTrainComboBox(box);
690        OperationsPanel.padComboBox(box);
691        return box;
692    }
693
694    public void updateTrainComboBox(JComboBox<Train> box) {
695        box.removeAllItems();
696        box.addItem(null);
697        for (Train train : getTrainsByNameList()) {
698            box.addItem(train);
699        }
700    }
701
702    /**
703     * Update combo box with trains that will service this car
704     *
705     * @param box the combo box to update
706     * @param car the car to be serviced
707     */
708    public void updateTrainComboBox(JComboBox<Train> box, Car car) {
709        box.removeAllItems();
710        box.addItem(null);
711        for (Train train : getTrainsByNameList()) {
712            if (train.isServiceable(car)) {
713                box.addItem(train);
714            }
715        }
716    }
717
718    public boolean isRowColorManual() {
719        return _rowColorManual;
720    }
721
722    public void setRowColorsManual(boolean manual) {
723        boolean old = _rowColorManual;
724        _rowColorManual = manual;
725        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, manual);
726    }
727
728    public String getRowColorNameForBuilt() {
729        return _rowColorBuilt;
730    }
731
732    public void setRowColorNameForBuilt(String colorName) {
733        String old = _rowColorBuilt;
734        _rowColorBuilt = colorName;
735        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, colorName);
736    }
737
738    public String getRowColorNameForBuildFailed() {
739        return _rowColorBuildFailed;
740    }
741
742    public void setRowColorNameForBuildFailed(String colorName) {
743        String old = _rowColorBuildFailed;
744        _rowColorBuildFailed = colorName;
745        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, colorName);
746    }
747
748    public String getRowColorNameForTrainEnRoute() {
749        return _rowColorTrainEnRoute;
750    }
751
752    public void setRowColorNameForTrainEnRoute(String colorName) {
753        String old = _rowColorTrainEnRoute;
754        _rowColorTrainEnRoute = colorName;
755        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, colorName);
756    }
757
758    public String getRowColorNameForTerminated() {
759        return _rowColorTerminated;
760    }
761
762    public void setRowColorNameForTerminated(String colorName) {
763        String old = _rowColorTerminated;
764        _rowColorTerminated = colorName;
765        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, colorName);
766    }
767    
768    public String getRowColorNameForReset() {
769        return _rowColorReset;
770    }
771
772    public void setRowColorNameForReset(String colorName) {
773        String old = _rowColorReset;
774        _rowColorReset = colorName;
775        setDirtyAndFirePropertyChange(ROW_COLOR_NAME_CHANGED_PROPERTY, old, colorName);
776    }
777
778    /**
779     * JColorChooser is not a replacement for getRowColorComboBox as it doesn't
780     * support no color as a selection.
781     * 
782     * @return the available colors used highlighting table rows including no color.
783     */
784    public JComboBox<String> getRowColorComboBox() {
785        JComboBox<String> box = new JComboBox<>();
786        box.addItem(NONE);
787        box.addItem(ColorUtil.ColorBlack);
788        box.addItem(ColorUtil.ColorRed);
789        box.addItem(ColorUtil.ColorPink);
790        box.addItem(ColorUtil.ColorOrange);
791        box.addItem(ColorUtil.ColorYellow);
792        box.addItem(ColorUtil.ColorGreen);
793        box.addItem(ColorUtil.ColorMagenta);
794        box.addItem(ColorUtil.ColorCyan);
795        box.addItem(ColorUtil.ColorBlue);
796        box.addItem(ColorUtil.ColorGray);
797        return box;
798    }
799
800    /**
801     * Makes a copy of an existing train.
802     *
803     * @param train     the train to copy
804     * @param trainName the name of the new train
805     * @return a copy of train
806     */
807    public Train copyTrain(Train train, String trainName) {
808        Train newTrain = newTrain(trainName);
809        // route, departure time and types
810        newTrain.setRoute(train.getRoute());
811        newTrain.setTrainSkipsLocations(train.getTrainSkipsLocations());
812        newTrain.setDepartureTime(train.getDepartureTimeHour(), train.getDepartureTimeMinute());
813        newTrain._typeList.clear(); // remove all types loaded by create
814        newTrain.setTypeNames(train.getTypeNames());
815        // set road, load, and owner options
816        newTrain.setCarRoadOption(train.getCarRoadOption());
817        newTrain.setCarRoadNames(train.getCarRoadNames());
818        newTrain.setLocoRoadOption(train.getLocoRoadOption());
819        newTrain.setLocoRoadNames(train.getLocoRoadNames());
820        newTrain.setLoadOption(train.getLoadOption());
821        newTrain.setLoadNames(train.getLoadNames());
822        newTrain.setOwnerOption(train.getOwnerOption());
823        newTrain.setOwnerNames(train.getOwnerNames());
824        // build dates
825        newTrain.setBuiltStartYear(train.getBuiltStartYear());
826        newTrain.setBuiltEndYear(train.getBuiltEndYear());
827        // locos start of route
828        newTrain.setNumberEngines(train.getNumberEngines());
829        newTrain.setEngineModel(train.getEngineModel());
830        newTrain.setEngineRoad(train.getEngineRoad());
831        newTrain.setRequirements(train.getRequirements());
832        newTrain.setCabooseRoad(train.getCabooseRoad());
833        // second leg
834        newTrain.setSecondLegNumberEngines(train.getSecondLegNumberEngines());
835        newTrain.setSecondLegEngineModel(train.getSecondLegEngineModel());
836        newTrain.setSecondLegEngineRoad(train.getSecondLegEngineRoad());
837        newTrain.setSecondLegOptions(train.getSecondLegOptions());
838        newTrain.setSecondLegCabooseRoad(train.getSecondLegCabooseRoad());
839        newTrain.setSecondLegStartRouteLocation(train.getSecondLegStartRouteLocation());
840        newTrain.setSecondLegEndRouteLocation(train.getSecondLegEndRouteLocation());
841        // third leg
842        newTrain.setThirdLegNumberEngines(train.getThirdLegNumberEngines());
843        newTrain.setThirdLegEngineModel(train.getThirdLegEngineModel());
844        newTrain.setThirdLegEngineRoad(train.getThirdLegEngineRoad());
845        newTrain.setThirdLegOptions(train.getThirdLegOptions());
846        newTrain.setThirdLegCabooseRoad(train.getThirdLegCabooseRoad());
847        newTrain.setThirdLegStartRouteLocation(train.getThirdLegStartRouteLocation());
848        newTrain.setThirdLegEndRouteLocation(train.getThirdLegEndRouteLocation());
849        // scripts
850        for (String scriptName : train.getBuildScripts()) {
851            newTrain.addBuildScript(scriptName);
852        }
853        for (String scriptName : train.getMoveScripts()) {
854            newTrain.addMoveScript(scriptName);
855        }
856        for (String scriptName : train.getTerminationScripts()) {
857            newTrain.addTerminationScript(scriptName);
858        }
859        // manifest options
860        newTrain.setRailroadName(train.getRailroadName());
861        newTrain.setManifestLogoPathName(train.getManifestLogoPathName());
862        newTrain.setShowArrivalAndDepartureTimes(train.isShowArrivalAndDepartureTimesEnabled());
863        // build options
864        newTrain.setAllowLocalMovesEnabled(train.isAllowLocalMovesEnabled());
865        newTrain.setAllowReturnToStagingEnabled(train.isAllowReturnToStagingEnabled());
866        newTrain.setAllowThroughCarsEnabled(train.isAllowThroughCarsEnabled());
867        newTrain.setBuildConsistEnabled(train.isBuildConsistEnabled());
868        newTrain.setSendCarsWithCustomLoadsToStagingEnabled(train.isSendCarsWithCustomLoadsToStagingEnabled());
869        newTrain.setBuildTrainNormalEnabled(train.isBuildTrainNormalEnabled());
870        newTrain.setSendCarsToTerminalEnabled(train.isSendCarsToTerminalEnabled());
871        newTrain.setServiceAllCarsWithFinalDestinationsEnabled(train.isServiceAllCarsWithFinalDestinationsEnabled());
872        // comment
873        newTrain.setComment(train.getCommentWithColor());
874        // description
875        newTrain.setDescription(train.getRawDescription());
876        return newTrain;
877    }
878
879    /**
880     * Provides a list of trains ordered by arrival time to a location
881     *
882     * @param location The location
883     * @return A list of trains ordered by arrival time.
884     */
885    public List<Train> getTrainsArrivingThisLocationList(Location location) {
886        // get a list of trains
887        List<Train> out = new ArrayList<>();
888        List<Integer> arrivalTimes = new ArrayList<>();
889        for (Train train : getTrainsByTimeList()) {
890            if (!train.isBuilt()) {
891                continue; // train wasn't built so skip
892            }
893            Route route = train.getRoute();
894            if (route == null) {
895                continue; // no route for this train
896            }
897            for (RouteLocation rl : route.getLocationsBySequenceList()) {
898                if (rl.getSplitName().equals(location.getSplitName())) {
899                    int expectedArrivalTime = train.getExpectedTravelTimeInMinutes(rl);
900                    // is already serviced then "-1"
901                    if (expectedArrivalTime == -1) {
902                        out.add(0, train); // place all trains that have already been serviced at the start
903                        arrivalTimes.add(0, expectedArrivalTime);
904                    } // if the train is in route, then expected arrival time is in minutes
905                    else if (train.isTrainEnRoute()) {
906                        for (int j = 0; j < out.size(); j++) {
907                            Train t = out.get(j);
908                            int time = arrivalTimes.get(j);
909                            if (t.isTrainEnRoute() && expectedArrivalTime < time) {
910                                out.add(j, train);
911                                arrivalTimes.add(j, expectedArrivalTime);
912                                break;
913                            }
914                            if (!t.isTrainEnRoute()) {
915                                out.add(j, train);
916                                arrivalTimes.add(j, expectedArrivalTime);
917                                break;
918                            }
919                        }
920                        // Train has not departed
921                    } else {
922                        for (int j = 0; j < out.size(); j++) {
923                            Train t = out.get(j);
924                            int time = arrivalTimes.get(j);
925                            if (!t.isTrainEnRoute() && expectedArrivalTime < time) {
926                                out.add(j, train);
927                                arrivalTimes.add(j, expectedArrivalTime);
928                                break;
929                            }
930                        }
931                    }
932                    if (!out.contains(train)) {
933                        out.add(train);
934                        arrivalTimes.add(expectedArrivalTime);
935                    }
936                    break; // done
937                }
938            }
939        }
940        return out;
941    }
942
943    /**
944     * Loads train icons if needed
945     */
946    public void loadTrainIcons() {
947        for (Train train : getTrainsByIdList()) {
948            train.loadTrainIcon();
949        }
950    }
951
952    /**
953     * Sets the switch list status for all built trains. Used for switch lists in
954     * consolidated mode.
955     *
956     * @param status Train.PRINTED, Train.UNKNOWN
957     */
958    public void setTrainsSwitchListStatus(String status) {
959        for (Train train : getTrainsByTimeList()) {
960            if (!train.isBuilt()) {
961                continue; // train isn't built so skip
962            }
963            train.setSwitchListStatus(status);
964        }
965    }
966
967    /**
968     * Sets all built trains manifests to modified. This causes the train's manifest
969     * to be recreated.
970     */
971    public void setTrainsModified() {
972        for (Train train : getTrainsByTimeList()) {
973            if (!train.isBuilt() || train.isTrainEnRoute()) {
974                continue; // train wasn't built or in route, so skip
975            }
976            train.setModified(true);
977        }
978    }
979
980    public void buildSelectedTrains(List<Train> trains) {
981        // use a thread to allow table updates during build
982        Thread build = jmri.util.ThreadingUtil.newThread(new Runnable() {
983            @Override
984            public void run() {
985                for (Train train : trains) {
986                    if (train.buildIfSelected()) {
987                        continue;
988                    }
989                    if (isBuildMessagesEnabled() && train.isBuildEnabled() && !train.isBuilt()) {
990                        if (JmriJOptionPane.showConfirmDialog(null, Bundle.getMessage("ContinueBuilding"),
991                                Bundle.getMessage("buildFailedMsg",
992                                        train.getName()),
993                                JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.NO_OPTION) {
994                            break;
995                        }
996                    }
997                }
998                setDirtyAndFirePropertyChange(TRAINS_BUILT_CHANGED_PROPERTY, false, true);
999            }
1000        });
1001        build.setName("Build Trains"); // NOI18N
1002        build.start();
1003    }
1004
1005    public boolean printSelectedTrains(List<Train> trains) {
1006        boolean status = true;
1007        for (Train train : trains) {
1008            if (train.isBuildEnabled()) {
1009                if (train.printManifestIfBuilt()) {
1010                    continue;
1011                }
1012                status = false; // failed to print all selected trains
1013                if (isBuildMessagesEnabled()) {
1014                    int response = JmriJOptionPane.showConfirmDialog(null,
1015                            Bundle.getMessage("NeedToBuildBeforePrinting",
1016                                    train.getName(),
1017                                            (isPrintPreviewEnabled() ? Bundle.getMessage("preview")
1018                                                    : Bundle.getMessage("print"))),
1019                            Bundle.getMessage("CanNotPrintManifest",
1020                                    isPrintPreviewEnabled() ? Bundle.getMessage("preview")
1021                                            : Bundle.getMessage("print")),
1022                            JmriJOptionPane.OK_CANCEL_OPTION);
1023                    if (response != JmriJOptionPane.OK_OPTION ) {
1024                        break;
1025                    }
1026                }
1027            }
1028        }
1029        return status;
1030    }
1031
1032    public boolean terminateSelectedTrains(List<Train> trains) {
1033        boolean status = true;
1034        for (Train train : trains) {
1035            if (train.isBuildEnabled() && train.isBuilt()) {
1036                if (train.isPrinted()) {
1037                    train.terminate();
1038                } else {
1039                    status = false;
1040                    int response = JmriJOptionPane.showConfirmDialog(null,
1041                            Bundle.getMessage("WarningTrainManifestNotPrinted"),
1042                            Bundle.getMessage("TerminateTrain",
1043                                    train.getName(), train.getDescription()),
1044                            JmriJOptionPane.YES_NO_CANCEL_OPTION);
1045                    if (response == JmriJOptionPane.YES_OPTION) {
1046                        train.terminate();
1047                    }
1048                    // else Quit?
1049                    if (response == JmriJOptionPane.CLOSED_OPTION || response == JmriJOptionPane.CANCEL_OPTION) {
1050                        break;
1051                    }
1052                }
1053            }
1054        }
1055        return status;
1056    }
1057
1058    public void resetBuildFailedTrains() {
1059        for (Train train : getList()) {
1060            if (train.isBuildFailed())
1061                train.reset();
1062        }
1063    }
1064
1065    int _maxTrainNameLength = 0;
1066
1067    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
1068            justification="I18N of Info Message")
1069    public int getMaxTrainNameLength() {
1070        String trainName = "";
1071        if (_maxTrainNameLength == 0) {
1072            for (Train train : getList()) {
1073                if (train.getName().length() > _maxTrainNameLength) {
1074                    trainName = train.getName();
1075                    _maxTrainNameLength = train.getName().length();
1076                }
1077            }
1078            log.info(Bundle.getMessage("InfoMaxName", trainName, _maxTrainNameLength));
1079        }
1080        return _maxTrainNameLength;
1081    }
1082
1083    public void load(Element root) {
1084        if (root.getChild(Xml.OPTIONS) != null) {
1085            Element options = root.getChild(Xml.OPTIONS);
1086            InstanceManager.getDefault(TrainCustomManifest.class).load(options);
1087            InstanceManager.getDefault(TrainCustomSwitchList.class).load(options);
1088            Element e = options.getChild(Xml.TRAIN_OPTIONS);
1089            Attribute a;
1090            if (e != null) {
1091                if ((a = e.getAttribute(Xml.BUILD_MESSAGES)) != null) {
1092                    _buildMessages = a.getValue().equals(Xml.TRUE);
1093                }
1094                if ((a = e.getAttribute(Xml.BUILD_REPORT)) != null) {
1095                    _buildReport = a.getValue().equals(Xml.TRUE);
1096                }
1097                if ((a = e.getAttribute(Xml.PRINT_PREVIEW)) != null) {
1098                    _printPreview = a.getValue().equals(Xml.TRUE);
1099                }
1100                if ((a = e.getAttribute(Xml.OPEN_FILE)) != null) {
1101                    _openFile = a.getValue().equals(Xml.TRUE);
1102                }
1103                if ((a = e.getAttribute(Xml.RUN_FILE)) != null) {
1104                    _runFile = a.getValue().equals(Xml.TRUE);
1105                }
1106                // verify that the Trains Window action is valid
1107                if ((a = e.getAttribute(Xml.TRAIN_ACTION)) != null &&
1108                        (a.getValue().equals(TrainsTableFrame.MOVE) ||
1109                                a.getValue().equals(TrainsTableFrame.RESET) ||
1110                                a.getValue().equals(TrainsTableFrame.TERMINATE) ||
1111                                a.getValue().equals(TrainsTableFrame.CONDUCTOR))) {
1112                    _trainAction = a.getValue();
1113                }
1114            }
1115
1116            // Conductor options
1117            Element eConductorOptions = options.getChild(Xml.CONDUCTOR_OPTIONS);
1118            if (eConductorOptions != null) {
1119                if ((a = eConductorOptions.getAttribute(Xml.SHOW_HYPHEN_NAME)) != null) {
1120                    _showLocationHyphenName = a.getValue().equals(Xml.TRUE);
1121                }
1122            }
1123
1124            // Row color options
1125            Element eRowColorOptions = options.getChild(Xml.ROW_COLOR_OPTIONS);
1126            if (eRowColorOptions != null) {
1127                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_MANUAL)) != null) {
1128                    _rowColorManual = a.getValue().equals(Xml.TRUE);
1129                }
1130                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_BUILD_FAILED)) != null) {
1131                    _rowColorBuildFailed = a.getValue().toLowerCase();
1132                }
1133                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_BUILT)) != null) {
1134                    _rowColorBuilt = a.getValue().toLowerCase();
1135                }
1136                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_TRAIN_EN_ROUTE)) != null) {
1137                    _rowColorTrainEnRoute = a.getValue().toLowerCase();
1138                }
1139                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_TERMINATED)) != null) {
1140                    _rowColorTerminated = a.getValue().toLowerCase();
1141                }
1142                if ((a = eRowColorOptions.getAttribute(Xml.ROW_COLOR_RESET)) != null) {
1143                    _rowColorReset = a.getValue().toLowerCase();
1144                }
1145            }
1146
1147            // moved to train schedule manager
1148            e = options.getChild(jmri.jmrit.operations.trains.schedules.Xml.TRAIN_SCHEDULE_OPTIONS);
1149            if (e != null) {
1150                if ((a = e.getAttribute(jmri.jmrit.operations.trains.schedules.Xml.ACTIVE_ID)) != null) {
1151                    InstanceManager.getDefault(TrainScheduleManager.class).setTrainScheduleActiveId(a.getValue());
1152                }
1153            }
1154            // check for scripts
1155            if (options.getChild(Xml.SCRIPTS) != null) {
1156                List<Element> lm = options.getChild(Xml.SCRIPTS).getChildren(Xml.START_UP);
1157                for (Element es : lm) {
1158                    if ((a = es.getAttribute(Xml.NAME)) != null) {
1159                        addStartUpScript(a.getValue());
1160                    }
1161                }
1162                List<Element> lt = options.getChild(Xml.SCRIPTS).getChildren(Xml.SHUT_DOWN);
1163                for (Element es : lt) {
1164                    if ((a = es.getAttribute(Xml.NAME)) != null) {
1165                        addShutDownScript(a.getValue());
1166                    }
1167                }
1168            }
1169        }
1170        if (root.getChild(Xml.TRAINS) != null) {
1171            List<Element> eTrains = root.getChild(Xml.TRAINS).getChildren(Xml.TRAIN);
1172            log.debug("readFile sees {} trains", eTrains.size());
1173            for (Element eTrain : eTrains) {
1174                register(new Train(eTrain));
1175            }
1176        }
1177    }
1178
1179    /**
1180     * Create an XML element to represent this Entry. This member has to remain
1181     * synchronized with the detailed DTD in operations-trains.dtd.
1182     *
1183     * @param root common Element for operations-trains.dtd.
1184     *
1185     */
1186    public void store(Element root) {
1187        Element options = new Element(Xml.OPTIONS);
1188        Element e = new Element(Xml.TRAIN_OPTIONS);
1189        e.setAttribute(Xml.BUILD_MESSAGES, isBuildMessagesEnabled() ? Xml.TRUE : Xml.FALSE);
1190        e.setAttribute(Xml.BUILD_REPORT, isBuildReportEnabled() ? Xml.TRUE : Xml.FALSE);
1191        e.setAttribute(Xml.PRINT_PREVIEW, isPrintPreviewEnabled() ? Xml.TRUE : Xml.FALSE);
1192        e.setAttribute(Xml.OPEN_FILE, isOpenFileEnabled() ? Xml.TRUE : Xml.FALSE);
1193        e.setAttribute(Xml.RUN_FILE, isRunFileEnabled() ? Xml.TRUE : Xml.FALSE);
1194        e.setAttribute(Xml.TRAIN_ACTION, getTrainsFrameTrainAction());
1195        options.addContent(e);
1196
1197        // Conductor options
1198        e = new Element(Xml.CONDUCTOR_OPTIONS);
1199        e.setAttribute(Xml.SHOW_HYPHEN_NAME, isShowLocationHyphenNameEnabled() ? Xml.TRUE : Xml.FALSE);
1200        options.addContent(e);
1201
1202        // Trains table row color options
1203        e = new Element(Xml.ROW_COLOR_OPTIONS);
1204        e.setAttribute(Xml.ROW_COLOR_MANUAL, isRowColorManual() ? Xml.TRUE : Xml.FALSE);
1205        e.setAttribute(Xml.ROW_COLOR_BUILD_FAILED, getRowColorNameForBuildFailed());
1206        e.setAttribute(Xml.ROW_COLOR_BUILT, getRowColorNameForBuilt());
1207        e.setAttribute(Xml.ROW_COLOR_TRAIN_EN_ROUTE, getRowColorNameForTrainEnRoute());
1208        e.setAttribute(Xml.ROW_COLOR_TERMINATED, getRowColorNameForTerminated());
1209        e.setAttribute(Xml.ROW_COLOR_RESET, getRowColorNameForReset());
1210        options.addContent(e);
1211
1212        if (getStartUpScripts().size() > 0 || getShutDownScripts().size() > 0) {
1213            // save list of shutdown scripts
1214            Element es = new Element(Xml.SCRIPTS);
1215            for (String scriptName : getStartUpScripts()) {
1216                Element em = new Element(Xml.START_UP);
1217                em.setAttribute(Xml.NAME, scriptName);
1218                es.addContent(em);
1219            }
1220            // save list of termination scripts
1221            for (String scriptName : getShutDownScripts()) {
1222                Element et = new Element(Xml.SHUT_DOWN);
1223                et.setAttribute(Xml.NAME, scriptName);
1224                es.addContent(et);
1225            }
1226            options.addContent(es);
1227        }
1228
1229        InstanceManager.getDefault(TrainCustomManifest.class).store(options); // save custom manifest elements
1230        InstanceManager.getDefault(TrainCustomSwitchList.class).store(options); // save custom switch list elements
1231
1232        root.addContent(options);
1233
1234        Element trains = new Element(Xml.TRAINS);
1235        root.addContent(trains);
1236        // add entries
1237        for (Train train : getTrainsByIdList()) {
1238            trains.addContent(train.store());
1239        }
1240        firePropertyChange(TRAINS_SAVED_PROPERTY, true, false);
1241    }
1242
1243    /**
1244     * Not currently used.
1245     */
1246    @Override
1247    public void propertyChange(java.beans.PropertyChangeEvent e) {
1248        log.debug("TrainManager sees property change: {} old: {} new: {}", e.getPropertyName(), e.getOldValue(),
1249                e.getNewValue());
1250    }
1251
1252    private void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1253        InstanceManager.getDefault(TrainManagerXml.class).setDirty(true);
1254        firePropertyChange(p, old, n);
1255    }
1256
1257    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrainManager.class);
1258
1259    @Override
1260    public void initialize() {
1261        InstanceManager.getDefault(OperationsSetupXml.class); // load setup
1262        InstanceManager.getDefault(TrainManagerXml.class); // load trains
1263    }
1264
1265}