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