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