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