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