001package jmri.jmrit.operations.trains;
002
003import java.awt.Color;
004import java.beans.PropertyChangeListener;
005import java.io.*;
006import java.text.MessageFormat;
007import java.time.LocalDateTime;
008import java.time.format.DateTimeFormatter;
009import java.util.*;
010
011import jmri.InstanceManager;
012import jmri.beans.Identifiable;
013import jmri.beans.PropertyChangeSupport;
014import jmri.jmrit.display.Editor;
015import jmri.jmrit.display.EditorManager;
016import jmri.jmrit.operations.locations.*;
017import jmri.jmrit.operations.rollingstock.RollingStock;
018import jmri.jmrit.operations.rollingstock.RollingStockManager;
019import jmri.jmrit.operations.rollingstock.cars.*;
020import jmri.jmrit.operations.rollingstock.engines.*;
021import jmri.jmrit.operations.routes.*;
022import jmri.jmrit.operations.setup.Control;
023import jmri.jmrit.operations.setup.Setup;
024import jmri.jmrit.operations.trains.csv.TrainCsvManifest;
025import jmri.jmrit.operations.trains.excel.TrainCustomManifest;
026import jmri.jmrit.operations.trains.trainbuilder.TrainBuilder;
027import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
028import jmri.jmrit.roster.RosterEntry;
029import jmri.script.JmriScriptEngineManager;
030import jmri.util.FileUtil;
031import jmri.util.swing.JmriJOptionPane;
032
033import org.jdom2.Element;
034
035/**
036 * Represents a train on the layout
037 *
038 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013,
039 *         2014, 2015, 2026
040 * @author Rodney Black Copyright (C) 2011
041 */
042public class Train extends PropertyChangeSupport implements Identifiable, PropertyChangeListener {
043
044    /*
045     * WARNING DO NOT LOAD CAR OR ENGINE MANAGERS WHEN Train.java IS CREATED IT
046     * CAUSES A RECURSIVE LOOP AT LOAD TIME, SEE EXAMPLES BELOW CarManager
047     * carManager = InstanceManager.getDefault(CarManager.class); EngineManager
048     * engineManager = InstanceManager.getDefault(EngineManager.class);
049     */
050
051    // The release date for JMRI operations 10/29/2008
052
053    public static final String NONE = "";
054
055    protected String _id = NONE;
056    protected String _name = NONE;
057    protected String _description = NONE;
058    protected RouteLocation _current = null;// where the train is located in its route
059    protected String _buildFailedMessage = NONE; // the build failed message for this train
060    protected boolean _built = false; // when true, a train manifest has been built
061    protected boolean _modified = false; // when true, user has modified train after being built
062    protected boolean _build = true; // when true, build this train
063    protected boolean _buildFailed = false; // when true, build for this train failed
064    protected boolean _printed = false; // when true, manifest has been printed
065    protected boolean _sendToTerminal = false; // when true, cars picked up by train only go to terminal
066    protected boolean _allowLocalMoves = true; // when true, cars with custom loads can be moved locally
067    protected boolean _allowThroughCars = true; // when true, cars from the origin can be sent to the terminal
068    protected boolean _buildNormal = false; // when true build this train in normal mode
069    protected boolean _allowCarsReturnStaging = false; // when true allow cars to return to staging
070    protected boolean _serviceAllCarsWithFinalDestinations = false; // when true, service cars with final destinations
071    protected boolean _buildConsist = false; // when true, build a consist for this train using single locomotives
072    protected boolean _sendCarsWithCustomLoadsToStaging = false; // when true, send cars to staging if spurs full
073    protected Route _route = null;
074    protected Track _departureTrack; // the departure track from staging
075    protected Track _terminationTrack; // the termination track into staging
076    protected String _carRoadOption = ALL_ROADS;// train car road name restrictions
077    protected List<String> _carRoadList = new ArrayList<>();
078    protected String _cabooseRoadOption = ALL_ROADS;// train caboose road name restrictions
079    protected List<String> _cabooseRoadList = new ArrayList<>();
080    protected String _locoRoadOption = ALL_ROADS;// train engine road name restrictions
081    protected List<String> _locoRoadList = new ArrayList<>();
082    protected int _requires = NO_CABOOSE_OR_FRED; // train requirements, caboose, FRED
083    protected String _numberEngines = "0"; // number of engines this train requires
084    protected String _engineRoad = NONE; // required road name for engines assigned to this train
085    protected String _engineModel = NONE; // required model of engines assigned to this train
086    protected String _cabooseRoad = NONE; // required road name for cabooses assigned to this train
087    protected String _departureTime = "0:00:00"; // departure time day:hour:minutes 
088    protected String _leadEngineId = NONE; // lead engine for train icon info
089    protected String _builtStartYear = NONE; // built start year
090    protected String _builtEndYear = NONE; // built end year
091    protected String _loadOption = ALL_LOADS;// train load restrictions
092    protected String _ownerOption = ALL_OWNERS;// train owner name restrictions
093    protected List<String> _buildScripts = new ArrayList<>(); // list of script pathnames to run before train is built
094    protected List<String> _afterBuildScripts = new ArrayList<>(); // script pathnames to run after train is built
095    protected List<String> _moveScripts = new ArrayList<>(); // list of script pathnames to run when train is moved
096    protected List<String> _terminationScripts = new ArrayList<>(); // script pathnames to run when train is terminated
097    protected String _railroadName = NONE; // optional railroad name for this train
098    protected String _logoPathName = NONE; // optional manifest logo for this train
099    protected boolean _showTimes = true; // when true, show arrival and departure times for this train
100    protected Engine _leadEngine = null; // lead engine for icon
101    protected String _switchListStatus = UNKNOWN; // print switch list status
102    protected String _comment = NONE;
103    protected String _serviceStatus = NONE; // status only if train is being built
104    protected int _statusCode = CODE_UNKNOWN;
105    protected int _oldStatusCode = CODE_UNKNOWN;
106    protected LocalDateTime _date; // date for last status change for this train
107    protected int _statusCarsRequested = 0;
108    protected String _tableRowColorName = NONE; // color of row in Trains table
109    protected String _tableRowColorResetName = NONE; // color of row in Trains table when reset
110
111    // Engine change and helper engines
112    protected int _leg2Options = NO_CABOOSE_OR_FRED; // options
113    protected RouteLocation _leg2Start = null; // route location when 2nd leg begins
114    protected RouteLocation _end2Leg = null; // route location where 2nd leg ends
115    protected String _leg2Engines = "0"; // number of engines 2nd leg
116    protected String _leg2Road = NONE; // engine road name 2nd leg
117    protected String _leg2Model = NONE; // engine model 2nd leg
118    protected String _leg2CabooseRoad = NONE; // road name for caboose 2nd leg
119
120    protected int _leg3Options = NO_CABOOSE_OR_FRED; // options
121    protected RouteLocation _leg3Start = null; // route location when 3rd leg begins
122    protected RouteLocation _leg3End = null; // route location where 3rd leg ends
123    protected String _leg3Engines = "0"; // number of engines 3rd leg
124    protected String _leg3Road = NONE; // engine road name 3rd leg
125    protected String _leg3Model = NONE; // engine model 3rd leg
126    protected String _leg3CabooseRoad = NONE; // road name for caboose 3rd leg
127
128    // engine change and helper options
129    public static final int CHANGE_ENGINES = 1; // change engines
130    public static final int HELPER_ENGINES = 2; // add helper engines
131    public static final int ADD_CABOOSE = 4; // add caboose
132    public static final int REMOVE_CABOOSE = 8; // remove caboose
133    public static final int ADD_ENGINES = 16; // add engines
134    public static final int REMOVE_ENGINES = 32; // remove engines
135
136    // property change names
137    public static final String DISPOSE_CHANGED_PROPERTY = "TrainDispose"; // NOI18N
138    public static final String STOPS_CHANGED_PROPERTY = "TrainStops"; // NOI18N
139    public static final String TYPES_CHANGED_PROPERTY = "TrainTypes"; // NOI18N
140    public static final String BUILT_CHANGED_PROPERTY = "TrainBuilt"; // NOI18N
141    public static final String BUILT_YEAR_CHANGED_PROPERTY = "TrainBuiltYear"; // NOI18N
142    public static final String BUILD_CHANGED_PROPERTY = "TrainBuild"; // NOI18N
143    public static final String ROADS_CHANGED_PROPERTY = "TrainRoads"; // NOI18N
144    public static final String LOADS_CHANGED_PROPERTY = "TrainLoads"; // NOI18N
145    public static final String OWNERS_CHANGED_PROPERTY = "TrainOwners"; // NOI18N
146    public static final String NAME_CHANGED_PROPERTY = "TrainName"; // NOI18N
147    public static final String DESCRIPTION_CHANGED_PROPERTY = "TrainDescription"; // NOI18N
148    public static final String STATUS_CHANGED_PROPERTY = "TrainStatus"; // NOI18N
149    public static final String DEPARTURETIME_CHANGED_PROPERTY = "TrainDepartureTime"; // NOI18N
150    public static final String TRAIN_LOCATION_CHANGED_PROPERTY = "TrainLocation"; // NOI18N
151    public static final String TRAIN_ROUTE_CHANGED_PROPERTY = "TrainRoute"; // NOI18N
152    public static final String TRAIN_REQUIREMENTS_CHANGED_PROPERTY = "TrainRequirements"; // NOI18N
153    public static final String TRAIN_MOVE_COMPLETE_CHANGED_PROPERTY = "TrainMoveComplete"; // NOI18N
154    public static final String TRAIN_ROW_COLOR_CHANGED_PROPERTY = "TrianRowColor"; // NOI18N
155    public static final String TRAIN_ROW_COLOR_RESET_CHANGED_PROPERTY = "TrianRowColorReset"; // NOI18N
156    public static final String TRAIN_MODIFIED_CHANGED_PROPERTY = "TrainModified"; // NOI18N
157    public static final String TRAIN_CURRENT_CHANGED_PROPERTY = "TrainCurrentLocation"; // NOI18N
158
159    // Train status
160    public static final String TRAIN_RESET = Bundle.getMessage("TrainReset");
161    public static final String RUN_SCRIPTS = Bundle.getMessage("RunScripts");
162    public static final String BUILDING = Bundle.getMessage("Building");
163    public static final String BUILD_FAILED = Bundle.getMessage("BuildFailed");
164    public static final String BUILT = Bundle.getMessage("Built");
165    public static final String PARTIAL_BUILT = Bundle.getMessage("Partial");
166    public static final String TRAIN_EN_ROUTE = Bundle.getMessage("TrainEnRoute");
167    public static final String TERMINATED = Bundle.getMessage("Terminated");
168    public static final String MANIFEST_MODIFIED = Bundle.getMessage("Modified");
169    public static final String ERROR = Bundle.getMessage("ErrorTitle");
170
171    // Train status codes
172    public static final int CODE_TRAIN_RESET = 0;
173    public static final int CODE_RUN_SCRIPTS = 0x100;
174    public static final int CODE_BUILDING = 0x01;
175    public static final int CODE_BUILD_FAILED = 0x02;
176    public static final int CODE_BUILT = 0x10;
177    public static final int CODE_PARTIAL_BUILT = CODE_BUILT + 0x04;
178    public static final int CODE_TRAIN_EN_ROUTE = CODE_BUILT + 0x08;
179    public static final int CODE_TERMINATED = 0x80;
180    public static final int CODE_MANIFEST_MODIFIED = 0x200;
181    public static final int CODE_ERROR = 0x400;
182    public static final int CODE_UNKNOWN = 0xFFFF;
183
184    // train requirements
185    public static final int NO_CABOOSE_OR_FRED = 0; // default
186    public static final int CABOOSE = 1;
187    public static final int FRED = 2;
188
189    // road options
190    public static final String ALL_ROADS = Bundle.getMessage("All");
191    public static final String INCLUDE_ROADS = Bundle.getMessage("Include");
192    public static final String EXCLUDE_ROADS = Bundle.getMessage("Exclude");
193
194    // owner options
195    public static final String ALL_OWNERS = Bundle.getMessage("All");
196    public static final String INCLUDE_OWNERS = Bundle.getMessage("Include");
197    public static final String EXCLUDE_OWNERS = Bundle.getMessage("Exclude");
198
199    // load options
200    public static final String ALL_LOADS = Bundle.getMessage("All");
201    public static final String INCLUDE_LOADS = Bundle.getMessage("Include");
202    public static final String EXCLUDE_LOADS = Bundle.getMessage("Exclude");
203
204    // Switch list status
205    public static final String UNKNOWN = "";
206    public static final String PRINTED = Bundle.getMessage("Printed");
207
208    public static final String AUTO = Bundle.getMessage("Auto");
209    public static final String AUTO_HPT = Bundle.getMessage("AutoHPT");
210    
211    // Train has serviced a location
212    public static final int SERVICED = -1;
213    public static final int NOT_PART_ROUTE = -2;
214
215    public Train(String id, String name) {
216        //       log.debug("New train ({}) id: {}", name, id);
217        _name = name;
218        _id = id;
219        // a new train accepts all types
220        setTypeNames(InstanceManager.getDefault(CarTypes.class).getNames());
221        setTypeNames(InstanceManager.getDefault(EngineTypes.class).getNames());
222        addPropertyChangeListerners();
223    }
224
225    @Override
226    public String getId() {
227        return _id;
228    }
229
230    /**
231     * Sets the name of this train, normally a short name that can fit within
232     * the train icon.
233     *
234     * @param name the train's name.
235     */
236    public void setName(String name) {
237        String old = _name;
238        _name = name;
239        if (!old.equals(name)) {
240            setDirtyAndFirePropertyChange(NAME_CHANGED_PROPERTY, old, name);
241        }
242    }
243
244    // for combo boxes
245    /**
246     * Get's a train's name
247     *
248     * @return train's name
249     */
250    @Override
251    public String toString() {
252        return _name;
253    }
254
255    /**
256     * Get's a train's name
257     *
258     * @return train's name
259     */
260    public String getName() {
261        return _name;
262    }
263
264    public String getSplitName() {
265        return TrainCommon.splitStringLeftParenthesis(getName());
266    }
267
268    /**
269     * @return The name of the color when highlighting the train's row
270     */
271    public String getTableRowColorName() {
272        return _tableRowColorName;
273    }
274
275    public void setTableRowColorName(String colorName) {
276        String old = _tableRowColorName;
277        _tableRowColorName = colorName;
278        if (!old.equals(colorName)) {
279            setDirtyAndFirePropertyChange(TRAIN_ROW_COLOR_CHANGED_PROPERTY, old, colorName);
280        }
281    }
282
283    /**
284     * @return The name of the train row color when the train is reset
285     */
286    public String getTableRowColorNameReset() {
287        return _tableRowColorResetName;
288    }
289
290    public void setTableRowColorNameReset(String colorName) {
291        String old = _tableRowColorResetName;
292        _tableRowColorResetName = colorName;
293        if (!old.equals(colorName)) {
294            setDirtyAndFirePropertyChange(TRAIN_ROW_COLOR_RESET_CHANGED_PROPERTY, old, colorName);
295        }
296    }
297
298    /**
299     * @return The color when highlighting the train's row
300     */
301    public Color getTableRowColor() {
302        String colorName = getTableRowColorName();
303        if (colorName.equals(NONE)) {
304            return null;
305        } else {
306            return Setup.getColor(colorName);
307        }
308    }
309
310    /**
311     * Get's train's departure time
312     *
313     * @return train's departure time in the String format dd:hh:mm
314     */
315    public String getDepartureTime() {
316        // check to see if the route has a departure time
317        RouteLocation rl = getTrainDepartsRouteLocation();
318        if (rl != null) {
319            rl.removePropertyChangeListener(this);
320            rl.addPropertyChangeListener(this);
321            if (!rl.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
322                return rl.getDepartureTime();
323            }
324        }
325        return _departureTime;
326    }
327
328    /**
329     * Get's train's departure time in 12hr or 24hr format
330     *
331     * @return train's departure time in the String format hh:mm or hh:mm AM/PM
332     */
333    public String getFormatedDepartureTime() {
334        return (parseTime(getDepartTimeMinutes()));
335    }
336
337    /**
338     * Get train's departure time in minutes from midnight for sorting
339     *
340     * @return int dd*24*60 + hh*60 + mm
341     */
342    public int getDepartTimeMinutes() {
343        int day = Integer.parseInt(getDepartureTimeDay());
344        int hour = Integer.parseInt(getDepartureTimeHour());
345        int minute = Integer.parseInt(getDepartureTimeMinute());
346        return (day * 24 * 60) + (hour * 60) + minute;
347    }
348
349    public void setDepartureTime(String day, String hour, String minute) {
350        String old = _departureTime;
351        hour = String.format("%02d", Integer.parseInt(hour));
352        minute = String.format("%02d", Integer.parseInt(minute));
353        String time = day + ":" + hour + ":" + minute;
354        _departureTime = time;
355        if (!old.equals(time)) {
356            setDirtyAndFirePropertyChange(DEPARTURETIME_CHANGED_PROPERTY, old, time);
357            setModified(true);
358        }
359    }
360    
361    public String getDepartureTimeDay() {
362        String[] time = getDepartureTime().split(":");
363        return time[0];
364    }
365
366    public String getDepartureTimeHour() {
367        String[] time = getDepartureTime().split(":");
368        return time[1];
369    }
370
371    public String getDepartureTimeMinute() {
372        String[] time = getDepartureTime().split(":");
373        return time[2];
374    }
375
376    public static final String ALREADY_SERVICED = "-1"; // NOI18N
377
378    /**
379     * Gets the expected time when this train will arrive at the location rl.
380     * Expected arrival time is based on the number of car pick up and set outs
381     * for this train. TODO Doesn't provide expected arrival time if train is in
382     * route, instead provides relative time. If train is at or has passed the
383     * location return -1.
384     *
385     * @param routeLocation The RouteLocation.
386     * @return expected arrival time in minutes (append AM or PM if 12 hour
387     *         format)
388     */
389    public String getExpectedArrivalTime(RouteLocation routeLocation) {
390        return getExpectedArrivalTime(routeLocation, false);
391    }
392
393    public String getExpectedArrivalTime(RouteLocation routeLocation, boolean isSortFormat) {
394        int minutes = getExpectedTravelTimeInMinutes(routeLocation);
395        if (minutes == Train.SERVICED) {
396            return ALREADY_SERVICED;
397        }
398        log.debug("Expected arrival time for train ({}) at ({}), {} minutes", getName(), routeLocation.getName(),
399                minutes);
400        // TODO use fast clock to get current time vs departure time
401        // for now use relative
402        return parseTime(minutes, isSortFormat);
403    }
404
405    public String getExpectedDepartureTime(RouteLocation routeLocation) {
406        return getExpectedDepartureTime(routeLocation, false);
407    }
408
409    public String getExpectedDepartureTime(RouteLocation routeLocation, boolean isSortFormat) {
410        int minutes = getExpectedTravelTimeInMinutes(routeLocation);
411        if (minutes == Train.SERVICED) {
412            minutes = 0; // provide the work time at routeLocation
413        }
414        if (routeLocation != null && !routeLocation.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
415            return parseTime(checkForDepartureTime(minutes, routeLocation), isSortFormat);
416        }
417        // figure out the work at this location, note that there can be
418        // consecutive locations with the same name
419        if (routeLocation != null && getRoute() != null) {
420            boolean foundRouteLocation = false;
421            for (RouteLocation rl : getRoute().getLocationsBySequenceList()) {
422                if (rl == routeLocation) {
423                    foundRouteLocation = true;
424                }
425                if (foundRouteLocation) {
426                    if (rl.getSplitName()
427                            .equals(routeLocation.getSplitName())) {
428                        minutes = minutes + getWorkTimeAtLocation(rl);
429                    } else {
430                        break; // done
431                    }
432                }
433            }
434        }
435//        log.debug("Expected departure time {} for train ({}) at ({})", minutes, getName(), routeLocation.getName());
436        return parseTime(minutes, isSortFormat);
437    }
438
439    public int getWorkTimeAtLocation(RouteLocation routeLocation) {
440        int minutes = 0;
441        // departure?
442        if (routeLocation == getTrainDepartsRouteLocation()) {
443            return minutes;
444        }
445        // add any work at this location
446        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
447            if (rs.getRouteLocation() == routeLocation && !rs.getTrackName().equals(RollingStock.NONE)) {
448                minutes += Setup.getSwitchTime();
449            }
450            if (rs.getRouteDestination() == routeLocation) {
451                minutes += Setup.getSwitchTime();
452            }
453        }
454        return minutes;
455    }
456
457    /**
458     * Used to determine when a train will arrive at a train's route location.
459     * Once a train departs, provides an estimated time in route and ignores the
460     * departure times from each route location.
461     * 
462     * @param routeLocation where in the train's route to get time
463     * @return Time in minutes
464     */
465    public int getExpectedTravelTimeInMinutes(RouteLocation routeLocation) {
466        int minutes = 0;
467        if (!isTrainEnRoute()) {
468            minutes += getDepartTimeMinutes();
469        } else {
470            minutes = SERVICED; // -1 means train has already served the location
471        }
472        // boolean trainAt = false;
473        boolean trainLocFound = false;
474        if (getRoute() != null) {
475            List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
476            for (int i = 0; i < routeList.size(); i++) {
477                RouteLocation rl = routeList.get(i);
478                if (rl == routeLocation) {
479                    break; // done
480                }
481                // start recording time after finding where the train is
482                if (!trainLocFound && isTrainEnRoute()) {
483                    if (rl == getCurrentRouteLocation()) {
484                        trainLocFound = true;
485                        // add travel time
486                        minutes = Setup.getTravelTime();
487                    }
488                    continue;
489                }
490                // is there a departure time from this location?
491                minutes = checkForDepartureTime(minutes, rl);
492                // add wait time
493                minutes += rl.getWait();
494                // add travel time if new location
495                try {
496                    RouteLocation next = routeList.get(i + 1);
497                    if (next != null &&
498                            !rl.getSplitName().equals(next.getSplitName())) {
499                        minutes += Setup.getTravelTime();
500                    }
501                } catch (IndexOutOfBoundsException e) {
502                    return NOT_PART_ROUTE;
503                }
504                // don't count work if there's a departure time
505                if (i == 0 || !rl.getDepartureTimeHourMinutes().equals(RouteLocation.NONE) && !isTrainEnRoute()) {
506                    continue;
507                }
508                // now add the work at the location
509                minutes += getWorkTimeAtLocation(rl);
510            }
511        }
512        return minutes;
513    }
514
515    private int checkForDepartureTime(int minutes, RouteLocation rl) {
516        if (!rl.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
517            int departMinute = 24 * 60 * Integer.parseInt(rl.getDepartureTimeDay()) +
518                    60 * Integer.parseInt(rl.getDepartureTimeHour()) +
519                    Integer.parseInt(rl.getDepartureTimeMinute());
520            // cross into new day?
521            if (minutes > departMinute) {
522                // yes
523                int days = 1 + minutes / (60 * 24);
524                departMinute += days * 60 * 24;
525            }
526            minutes = departMinute;
527        }
528        return minutes;
529    }
530
531    /**
532     * Returns time in days:hours:minutes format
533     *
534     * @param minutes number of minutes from midnight
535     * @return hour:minute (optionally AM:PM format)
536     */
537    private String parseTime(int minutes) {
538        return parseTime(minutes, false);
539    }
540
541    private String parseTime(int minutes, boolean isSortFormat) {
542        int hours = minutes / 60;
543        minutes = minutes - hours * 60;
544        int days = hours / 24;
545        hours = hours - days * 24;
546
547        String d = "";
548        if (isSortFormat) {
549            d = "0:";
550        }
551        
552        if (days > 0) {
553            d = Integer.toString(days) + ":";
554        }
555        
556        if (!isSortFormat) {
557            String nd = Setup.getDayToName(Integer.toString(days));
558            if (nd != null && !nd.isBlank()) {
559                d = nd + " ";
560            }
561        }
562
563        // AM_PM field
564        String am_pm = "";
565        if (Setup.is12hrFormatEnabled() && !isTrainEnRoute() && !isSortFormat) {
566            am_pm = TrainCommon.SPACE + Bundle.getMessage("AM");
567            if (hours >= 12) {
568                hours = hours - 12;
569                am_pm = TrainCommon.SPACE + Bundle.getMessage("PM");
570            }
571            if (hours == 0) {
572                hours = 12;
573            }
574        }
575        String h = String.format("%02d", hours);
576        String m = String.format("%02d", minutes);
577        return d + h + ":" + m + am_pm;
578    }
579
580    /**
581     * Set train requirements. If NO_CABOOSE_OR_FRED, then train doesn't require
582     * a caboose or car with FRED.
583     *
584     * @param requires NO_CABOOSE_OR_FRED, CABOOSE, FRED
585     */
586    public void setRequirements(int requires) {
587        int old = _requires;
588        _requires = requires;
589        if (old != requires) {
590            setDirtyAndFirePropertyChange(TRAIN_REQUIREMENTS_CHANGED_PROPERTY, old, requires);
591        }
592    }
593
594    /**
595     * Get a train's requirements with regards to the last car in the train.
596     *
597     * @return NONE CABOOSE FRED
598     */
599    public int getRequirements() {
600        return _requires;
601    }
602
603    public boolean isCabooseNeeded() {
604        return (getRequirements() & CABOOSE) == CABOOSE;
605    }
606
607    public boolean isFredNeeded() {
608        return (getRequirements() & FRED) == FRED;
609    }
610
611    public void setRoute(Route route) {
612        Route old = _route;
613        String oldRoute = NONE;
614        String newRoute = NONE;
615        if (old != null) {
616            old.removePropertyChangeListener(this);
617            oldRoute = old.toString();
618        }
619        if (route != null) {
620            route.addPropertyChangeListener(this);
621            newRoute = route.toString();
622        }
623        _route = route;
624        _skipLocationsList.clear();
625        if (old == null || !old.equals(route)) {
626            setDirtyAndFirePropertyChange(TRAIN_ROUTE_CHANGED_PROPERTY, oldRoute, newRoute);
627        }
628    }
629
630    /**
631     * Gets the train's route
632     *
633     * @return train's route
634     */
635    public Route getRoute() {
636        return _route;
637    }
638
639    /**
640     * Get's the train's route name.
641     *
642     * @return Train's route name.
643     */
644    public String getTrainRouteName() {
645        if (getRoute() == null) {
646            return NONE;
647        }
648        return getRoute().getName();
649    }
650
651    /**
652     * Get the train's departure location's name
653     *
654     * @return train's departure location's name
655     */
656    public String getTrainDepartsName() {
657        if (getTrainDepartsRouteLocation() != null) {
658            return getTrainDepartsRouteLocation().getName();
659        }
660        return NONE;
661    }
662
663    public RouteLocation getTrainDepartsRouteLocation() {
664        if (getRoute() == null) {
665            return null;
666        }
667        return getRoute().getDepartsRouteLocation();
668    }
669
670    public String getTrainDepartsDirection() {
671        String direction = NONE;
672        if (getTrainDepartsRouteLocation() != null) {
673            direction = getTrainDepartsRouteLocation().getTrainDirectionString();
674        }
675        return direction;
676    }
677
678    /**
679     * Get train's final location's name
680     *
681     * @return train's final location's name
682     */
683    public String getTrainTerminatesName() {
684        if (getTrainTerminatesRouteLocation() != null) {
685            return getTrainTerminatesRouteLocation().getName();
686        }
687        return NONE;
688    }
689
690    public RouteLocation getTrainTerminatesRouteLocation() {
691        if (getRoute() == null) {
692            return null;
693        }
694        return getRoute().getTerminatesRouteLocation();
695    }
696
697    /**
698     * Returns the order the train should be blocked.
699     *
700     * @return routeLocations for this train.
701     */
702    public List<RouteLocation> getTrainBlockingOrder() {
703        if (getRoute() == null) {
704            return null;
705        }
706        return getRoute().getBlockingOrder();
707    }
708
709    /**
710     * Set train's current route location
711     *
712     * @param location The current RouteLocation.
713     */
714    public void setCurrentLocation(RouteLocation location) {
715        RouteLocation old = _current;
716        _current = location;
717        if ((old != null && !old.equals(location)) || (old == null && location != null)) {
718            setDirtyAndFirePropertyChange(TRAIN_CURRENT_CHANGED_PROPERTY, old, location);
719        }
720    }
721
722    /**
723     * Get train's current location name
724     *
725     * @return Train's current route location name
726     */
727    public String getCurrentLocationName() {
728        if (getCurrentRouteLocation() == null) {
729            return NONE;
730        }
731        return getCurrentRouteLocation().getName();
732    }
733
734    /**
735     * Get train's current route location
736     *
737     * @return Train's current route location
738     */
739    public RouteLocation getCurrentRouteLocation() {
740        if (getRoute() == null) {
741            return null;
742        }
743        if (_current == null) {
744            return null;
745        }
746        // this will verify that the current location still exists
747        return getRoute().getRouteLocationById(_current.getId());
748    }
749
750    /**
751     * Get the train's next location name
752     *
753     * @return Train's next route location name
754     */
755    public String getNextLocationName() {
756        return getNextLocationName(1);
757    }
758
759    /**
760     * Get a location name in a train's route from the current train's location.
761     * A number of "1" means get the next location name in a train's route.
762     *
763     * @param number The stop number, must be greater than 0
764     * @return Name of the location that is the number of stops away from the
765     *         train's current location.
766     */
767    public String getNextLocationName(int number) {
768        RouteLocation rl = getCurrentRouteLocation();
769        while (number-- > 0) {
770            rl = getNextRouteLocation(rl);
771            if (rl == null) {
772                return NONE;
773            }
774        }
775        return rl.getName();
776    }
777
778    public RouteLocation getNextRouteLocation(RouteLocation currentRouteLocation) {
779        if (getRoute() == null) {
780            return null;
781        }
782        List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
783        for (int i = 0; i < routeList.size(); i++) {
784            RouteLocation rl = routeList.get(i);
785            if (rl == currentRouteLocation) {
786                i++;
787                if (i < routeList.size()) {
788                    return routeList.get(i);
789                }
790                break;
791            }
792        }
793        return null; // At end of route
794    }
795
796    public void setDepartureTrack(Track track) {
797        Track old = _departureTrack;
798        _departureTrack = track;
799        if (old != track) {
800            setDirtyAndFirePropertyChange("DepartureTrackChanged", old, track); // NOI18N
801        }
802    }
803
804    public Track getDepartureTrack() {
805        return _departureTrack;
806    }
807
808    public boolean isDepartingStaging() {
809        return getDepartureTrack() != null;
810    }
811
812    public void setTerminationTrack(Track track) {
813        Track old = _terminationTrack;
814        _terminationTrack = track;
815        if (old != track) {
816            setDirtyAndFirePropertyChange("TerminationTrackChanged", old, track); // NOI18N
817        }
818    }
819
820    public Track getTerminationTrack() {
821        return _terminationTrack;
822    }
823
824    /**
825     * Set the train's machine readable status. Calls update train table row
826     * color.
827     *
828     * @param code machine readable
829     */
830    public void setStatusCode(int code) {
831        String oldStatus = getStatus();
832        int oldCode = getStatusCode();
833        _statusCode = code;
834        setDate(LocalDateTime.now());
835        if (oldCode != getStatusCode()) {
836            setDirtyAndFirePropertyChange(STATUS_CHANGED_PROPERTY, oldStatus, getStatus());
837        }
838        updateTrainTableRowColor();
839    }
840
841    public void updateTrainTableRowColor() {
842        if (!InstanceManager.getDefault(TrainManager.class).isRowColorManual()) {
843            switch (getStatusCode()) {
844                case CODE_TRAIN_RESET:
845                    String color = getTableRowColorNameReset();
846                    if (color.equals(NONE)) {
847                        color = InstanceManager.getDefault(TrainManager.class).getRowColorNameForReset();
848                    }
849                    setTableRowColorName(color);
850                    break;
851                case CODE_BUILT:
852                case CODE_PARTIAL_BUILT:
853                    setTableRowColorName(InstanceManager.getDefault(TrainManager.class).getRowColorNameForBuilt());
854                    break;
855                case CODE_BUILD_FAILED:
856                    setTableRowColorName(
857                            InstanceManager.getDefault(TrainManager.class).getRowColorNameForBuildFailed());
858                    break;
859                case CODE_TRAIN_EN_ROUTE:
860                    setTableRowColorName(
861                            InstanceManager.getDefault(TrainManager.class).getRowColorNameForTrainEnRoute());
862                    break;
863                case CODE_TERMINATED:
864                    setTableRowColorName(InstanceManager.getDefault(TrainManager.class).getRowColorNameForTerminated());
865                    break;
866                default: // all other cases do nothing
867                    break;
868            }
869        }
870    }
871
872    /**
873     * Get train's status in the default locale.
874     *
875     * @return Human-readable status
876     */
877    public String getStatus() {
878        return this.getStatus(Locale.getDefault());
879    }
880
881    /**
882     * Get train's status in the specified locale.
883     *
884     * @param locale The Locale.
885     * @return Human-readable status
886     */
887    public String getStatus(Locale locale) {
888        return this.getStatus(locale, this.getStatusCode());
889    }
890
891    /**
892     * Get the human-readable status for the requested status code.
893     *
894     * @param locale The Locale.
895     * @param code   requested status
896     * @return Human-readable status
897     */
898    public String getStatus(Locale locale, int code) {
899        switch (code) {
900            case CODE_RUN_SCRIPTS:
901                return RUN_SCRIPTS;
902            case CODE_BUILDING:
903                return BUILDING;
904            case CODE_BUILD_FAILED:
905                return BUILD_FAILED;
906            case CODE_BUILT:
907                return Bundle.getMessage(locale, "StatusBuilt", this.getNumberCarsWorked()); // NOI18N
908            case CODE_PARTIAL_BUILT:
909                return Bundle.getMessage(locale, "StatusPartialBuilt", this.getNumberCarsWorked(),
910                        this.getNumberCarsRequested()); // NOI18N
911            case CODE_TERMINATED:
912                return Bundle.getMessage(locale, "StatusTerminated", this.getSortDate()); // NOI18N
913            case CODE_TRAIN_EN_ROUTE:
914                return Bundle.getMessage(locale, "StatusEnRoute", this.getNumberCarsInTrain(), this.getTrainLength(),
915                        Setup.getLengthUnit().toLowerCase(), this.getTrainWeight()); // NOI18N
916            case CODE_TRAIN_RESET:
917                return TRAIN_RESET;
918            case CODE_MANIFEST_MODIFIED:
919                return MANIFEST_MODIFIED;
920            case CODE_ERROR:
921                return ERROR;
922            case CODE_UNKNOWN:
923            default:
924                return UNKNOWN;
925        }
926    }
927
928    public String getMRStatus() {
929        switch (getStatusCode()) {
930            case CODE_PARTIAL_BUILT:
931                return getStatusCode() + "||" + this.getNumberCarsRequested(); // NOI18N
932            case CODE_TERMINATED:
933                return getStatusCode() + "||" + this.getSortDate(); // NOI18N
934            default:
935                return Integer.toString(getStatusCode());
936        }
937    }
938
939    public int getStatusCode() {
940        return _statusCode;
941    }
942
943    protected void setOldStatusCode(int code) {
944        _oldStatusCode = code;
945    }
946
947    protected int getOldStatusCode() {
948        return _oldStatusCode;
949    }
950
951    /**
952     * Used to determine if train has departed the first location in the train's
953     * route
954     *
955     * @return true if train has departed
956     */
957    public boolean isTrainEnRoute() {
958        return !getCurrentLocationName().equals(NONE) && getTrainDepartsRouteLocation() != getCurrentRouteLocation();
959    }
960
961    /**
962     * Used to determine if train is a local switcher serving one location. Note
963     * the train can have more than location in its route, but all location
964     * names must be "same". See TrainCommon.splitString(String name) for the
965     * definition of the "same" name.
966     *
967     * @return true if local switcher
968     */
969    public boolean isLocalSwitcher() {
970        String departureName = TrainCommon.splitString(getTrainDepartsName());
971        Route route = getRoute();
972        if (route != null) {
973            for (RouteLocation rl : route.getLocationsBySequenceList()) {
974                if (!departureName.equals(rl.getSplitName())) {
975                    return false; // not a local switcher
976                }
977            }
978        }
979        return true;
980    }
981
982    public boolean isTurn() {
983        return !isLocalSwitcher() &&
984                TrainCommon.splitString(getTrainDepartsName())
985                        .equals(TrainCommon.splitString(getTrainTerminatesName()));
986    }
987
988    /**
989     * Used to determine if train is carrying only passenger cars.
990     *
991     * @return true if only passenger cars have been assigned to this train.
992     */
993    public boolean isOnlyPassengerCars() {
994        for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
995            if (!car.isPassenger()) {
996                return false;
997            }
998        }
999        return true;
1000    }
1001
1002    List<String> _skipLocationsList = new ArrayList<>();
1003
1004    protected String[] getTrainSkipsLocations() {
1005        String[] locationIds = new String[_skipLocationsList.size()];
1006        for (int i = 0; i < _skipLocationsList.size(); i++) {
1007            locationIds[i] = _skipLocationsList.get(i);
1008        }
1009        return locationIds;
1010    }
1011
1012    protected void setTrainSkipsLocations(String[] locationIds) {
1013        if (locationIds.length > 0) {
1014            Arrays.sort(locationIds);
1015            for (String id : locationIds) {
1016                _skipLocationsList.add(id);
1017            }
1018        }
1019    }
1020
1021    /**
1022     * Train will skip the RouteLocation
1023     *
1024     * @param rl RouteLocation
1025     */
1026    public void addTrainSkipsLocation(RouteLocation rl) {
1027        // insert at start of _skipLocationsList, sort later
1028        if (!_skipLocationsList.contains(rl.getId())) {
1029            _skipLocationsList.add(0, rl.getId());
1030            setDirtyAndFirePropertyChange(STOPS_CHANGED_PROPERTY, _skipLocationsList.size() - 1,
1031                    _skipLocationsList.size());
1032        }
1033    }
1034
1035    public void deleteTrainSkipsLocation(RouteLocation rl) {
1036        _skipLocationsList.remove(rl.getId());
1037        setDirtyAndFirePropertyChange(STOPS_CHANGED_PROPERTY, _skipLocationsList.size() + 1, _skipLocationsList.size());
1038    }
1039
1040    /**
1041     * Determines if this train skips a location (doesn't service the location).
1042     *
1043     * @param rl The route location.
1044     * @return true if the train will not service the location.
1045     */
1046    public boolean isLocationSkipped(RouteLocation rl) {
1047        return _skipLocationsList.contains(rl.getId());
1048    }
1049
1050    List<String> _typeList = new ArrayList<>();
1051
1052    /**
1053     * Get's the type names of rolling stock this train will service
1054     *
1055     * @return The type names for cars and or engines
1056     */
1057    public String[] getTypeNames() {
1058        return _typeList.toArray(new String[0]);
1059    }
1060
1061    public String[] getCarTypeNames() {
1062        List<String> list = new ArrayList<>();
1063        for (String type : _typeList) {
1064            if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
1065                list.add(type);
1066            }
1067        }
1068        return list.toArray(new String[0]);
1069    }
1070
1071    public String[] getLocoTypeNames() {
1072        List<String> list = new ArrayList<>();
1073        for (String type : _typeList) {
1074            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
1075                list.add(type);
1076            }
1077        }
1078        return list.toArray(new String[0]);
1079    }
1080
1081    /**
1082     * Set the type of cars or engines this train will service, see types in
1083     * Cars and Engines.
1084     *
1085     * @param types The type names for cars and or engines
1086     */
1087    protected void setTypeNames(String[] types) {
1088        if (types.length > 0) {
1089            Arrays.sort(types);
1090            for (String type : types) {
1091                _typeList.add(type);
1092            }
1093        }
1094    }
1095
1096    /**
1097     * Add a car or engine type name that this train will service.
1098     *
1099     * @param type The new type name to service.
1100     */
1101    public void addTypeName(String type) {
1102        // insert at start of list, sort later
1103        if (type == null || _typeList.contains(type)) {
1104            return;
1105        }
1106        _typeList.add(0, type);
1107        log.debug("Train ({}) add car type ({})", getName(), type);
1108        setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() - 1, _typeList.size());
1109    }
1110
1111    public void deleteTypeName(String type) {
1112        if (_typeList.remove(type)) {
1113            log.debug("Train ({}) delete car type ({})", getName(), type);
1114            setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() + 1, _typeList.size());
1115        }
1116    }
1117
1118    /**
1119     * Returns true if this train will service the type of car or engine.
1120     *
1121     * @param type The car or engine type name.
1122     * @return true if this train will service the particular type.
1123     */
1124    public boolean isTypeNameAccepted(String type) {
1125        return _typeList.contains(type);
1126    }
1127
1128    protected void replaceType(String oldType, String newType) {
1129        if (isTypeNameAccepted(oldType)) {
1130            deleteTypeName(oldType);
1131            addTypeName(newType);
1132            // adjust loads with type in them
1133            for (String load : getLoadNames()) {
1134                String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
1135                if (splitLoad.length > 1) {
1136                    if (splitLoad[0].equals(oldType)) {
1137                        deleteLoadName(load);
1138                        if (newType != null) {
1139                            load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
1140                            addLoadName(load);
1141                        }
1142                    }
1143                }
1144            }
1145        }
1146    }
1147
1148    /**
1149     * Get how this train deals with car road names.
1150     *
1151     * @return ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1152     */
1153    public String getCarRoadOption() {
1154        return _carRoadOption;
1155    }
1156
1157    /**
1158     * Set how this train deals with car road names.
1159     *
1160     * @param option ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1161     */
1162    public void setCarRoadOption(String option) {
1163        String old = _carRoadOption;
1164        _carRoadOption = option;
1165        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
1166    }
1167
1168    public void setCarRoadNames(String[] roads) {
1169        setRoadNames(roads, _carRoadList);
1170    }
1171
1172    /**
1173     * Provides a list of car road names that the train will either service or
1174     * exclude. See setCarRoadOption
1175     *
1176     * @return Array of sorted road names as Strings
1177     */
1178    public String[] getCarRoadNames() {
1179        String[] roads = _carRoadList.toArray(new String[0]);
1180        if (_carRoadList.size() > 0) {
1181            Arrays.sort(roads);
1182        }
1183        return roads;
1184    }
1185
1186    /**
1187     * Add a car road name that the train will either service or exclude. See
1188     * setCarRoadOption
1189     *
1190     * @param road The string road name.
1191     * @return true if road name was added, false if road name wasn't in the
1192     *         list.
1193     */
1194    public boolean addCarRoadName(String road) {
1195        if (_carRoadList.contains(road)) {
1196            return false;
1197        }
1198        _carRoadList.add(road);
1199        log.debug("train ({}) add car road {}", getName(), road);
1200        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _carRoadList.size() - 1, _carRoadList.size());
1201        return true;
1202    }
1203
1204    /**
1205     * Delete a car road name that the train will either service or exclude. See
1206     * setRoadOption
1207     *
1208     * @param road The string road name to delete.
1209     * @return true if road name was removed, false if road name wasn't in the
1210     *         list.
1211     */
1212    public boolean deleteCarRoadName(String road) {
1213        if (_carRoadList.remove(road)) {
1214            log.debug("train ({}) delete car road {}", getName(), road);
1215            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _carRoadList.size() + 1, _carRoadList.size());
1216            return true;
1217        }
1218        return false;
1219    }
1220
1221    /**
1222     * Determine if train will service a specific road name for a car.
1223     *
1224     * @param road the road name to check.
1225     * @return true if train will service this road name.
1226     */
1227    public boolean isCarRoadNameAccepted(String road) {
1228        if (_carRoadOption.equals(ALL_ROADS)) {
1229            return true;
1230        }
1231        if (_carRoadOption.equals(INCLUDE_ROADS)) {
1232            return _carRoadList.contains(road);
1233        }
1234        // exclude!
1235        return !_carRoadList.contains(road);
1236    }
1237
1238    /**
1239     * Get how this train deals with caboose road names.
1240     *
1241     * @return ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1242     */
1243    public String getCabooseRoadOption() {
1244        return _cabooseRoadOption;
1245    }
1246
1247    /**
1248     * Set how this train deals with caboose road names.
1249     *
1250     * @param option ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1251     */
1252    public void setCabooseRoadOption(String option) {
1253        String old = _cabooseRoadOption;
1254        _cabooseRoadOption = option;
1255        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
1256    }
1257
1258    protected void setCabooseRoadNames(String[] roads) {
1259        setRoadNames(roads, _cabooseRoadList);
1260    }
1261
1262    /**
1263     * Provides a list of caboose road names that the train will either service
1264     * or exclude. See setCabooseRoadOption
1265     *
1266     * @return Array of sorted road names as Strings
1267     */
1268    public String[] getCabooseRoadNames() {
1269        String[] roads = _cabooseRoadList.toArray(new String[0]);
1270        if (_cabooseRoadList.size() > 0) {
1271            Arrays.sort(roads);
1272        }
1273        return roads;
1274    }
1275
1276    /**
1277     * Add a caboose road name that the train will either service or exclude.
1278     * See setCabooseRoadOption
1279     *
1280     * @param road The string road name.
1281     * @return true if road name was added, false if road name wasn't in the
1282     *         list.
1283     */
1284    public boolean addCabooseRoadName(String road) {
1285        if (_cabooseRoadList.contains(road)) {
1286            return false;
1287        }
1288        _cabooseRoadList.add(road);
1289        log.debug("train ({}) add caboose road {}", getName(), road);
1290        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _cabooseRoadList.size() - 1, _cabooseRoadList.size());
1291        return true;
1292    }
1293
1294    /**
1295     * Delete a caboose road name that the train will either service or exclude.
1296     * See setRoadOption
1297     *
1298     * @param road The string road name to delete.
1299     * @return true if road name was removed, false if road name wasn't in the
1300     *         list.
1301     */
1302    public boolean deleteCabooseRoadName(String road) {
1303        if (_cabooseRoadList.remove(road)) {
1304            log.debug("train ({}) delete caboose road {}", getName(), road);
1305            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _cabooseRoadList.size() + 1, _cabooseRoadList.size());
1306            return true;
1307        }
1308        return false;
1309    }
1310
1311    /**
1312     * Determine if train will service a specific road name for a caboose.
1313     *
1314     * @param road the road name to check.
1315     * @return true if train will service this road name.
1316     */
1317    public boolean isCabooseRoadNameAccepted(String road) {
1318        if (_cabooseRoadOption.equals(ALL_ROADS)) {
1319            return true;
1320        }
1321        if (_cabooseRoadOption.equals(INCLUDE_ROADS)) {
1322            return _cabooseRoadList.contains(road);
1323        }
1324        // exclude!
1325        return !_cabooseRoadList.contains(road);
1326    }
1327
1328    /**
1329     * Get how this train deals with locomotive road names.
1330     *
1331     * @return ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1332     */
1333    public String getLocoRoadOption() {
1334        return _locoRoadOption;
1335    }
1336
1337    /**
1338     * Set how this train deals with locomotive road names.
1339     *
1340     * @param option ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
1341     */
1342    public void setLocoRoadOption(String option) {
1343        String old = _locoRoadOption;
1344        _locoRoadOption = option;
1345        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
1346    }
1347
1348    public void setLocoRoadNames(String[] roads) {
1349        setRoadNames(roads, _locoRoadList);
1350    }
1351
1352    private void setRoadNames(String[] roads, List<String> list) {
1353        if (roads.length > 0) {
1354            Arrays.sort(roads);
1355            for (String road : roads) {
1356                if (!road.isEmpty()) {
1357                    list.add(road);
1358                }
1359            }
1360        }
1361    }
1362
1363    /**
1364     * Provides a list of engine road names that the train will either service
1365     * or exclude. See setLocoRoadOption
1366     *
1367     * @return Array of sorted road names as Strings
1368     */
1369    public String[] getLocoRoadNames() {
1370        String[] roads = _locoRoadList.toArray(new String[0]);
1371        if (_locoRoadList.size() > 0) {
1372            Arrays.sort(roads);
1373        }
1374        return roads;
1375    }
1376
1377    /**
1378     * Add a engine road name that the train will either service or exclude. See
1379     * setLocoRoadOption
1380     *
1381     * @param road The string road name.
1382     * @return true if road name was added, false if road name wasn't in the
1383     *         list.
1384     */
1385    public boolean addLocoRoadName(String road) {
1386        if (road.isBlank() || _locoRoadList.contains(road)) {
1387            return false;
1388        }
1389        _locoRoadList.add(road);
1390        log.debug("train ({}) add engine road {}", getName(), road);
1391        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _locoRoadList.size() - 1, _locoRoadList.size());
1392        return true;
1393    }
1394
1395    /**
1396     * Delete a engine road name that the train will either service or exclude.
1397     * See setLocoRoadOption
1398     *
1399     * @param road The string road name to delete.
1400     * @return true if road name was removed, false if road name wasn't in the
1401     *         list.
1402     */
1403    public boolean deleteLocoRoadName(String road) {
1404        if (_locoRoadList.remove(road)) {
1405            log.debug("train ({}) delete engine road {}", getName(), road);
1406            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _locoRoadList.size() + 1, _locoRoadList.size());
1407            return true;
1408        }
1409        return false;
1410    }
1411
1412    /**
1413     * Determine if train will service a specific road name for an engine.
1414     *
1415     * @param road the road name to check.
1416     * @return true if train will service this road name.
1417     */
1418    public boolean isLocoRoadNameAccepted(String road) {
1419        if (_locoRoadOption.equals(ALL_ROADS)) {
1420            return true;
1421        }
1422        if (_locoRoadOption.equals(INCLUDE_ROADS)) {
1423            return _locoRoadList.contains(road);
1424        }
1425        // exclude!
1426        return !_locoRoadList.contains(road);
1427    }
1428
1429    protected void replaceRoad(String oldRoad, String newRoad) {
1430        if (newRoad != null) {
1431            if (deleteCarRoadName(oldRoad)) {
1432                addCarRoadName(newRoad);
1433            }
1434            if (deleteCabooseRoadName(oldRoad)) {
1435                addCabooseRoadName(newRoad);
1436            }
1437            if (deleteLocoRoadName(oldRoad)) {
1438                addLocoRoadName(newRoad);
1439            }
1440            if (getEngineRoad().equals(oldRoad)) {
1441                setEngineRoad(newRoad);
1442            }
1443            if (getCabooseRoad().equals(oldRoad)) {
1444                setCabooseRoad(newRoad);
1445            }
1446            if (getSecondLegEngineRoad().equals(oldRoad)) {
1447                setSecondLegEngineRoad(newRoad);
1448            }
1449            if (getSecondLegCabooseRoad().equals(oldRoad)) {
1450                setSecondLegCabooseRoad(newRoad);
1451            }
1452            if (getThirdLegEngineRoad().equals(oldRoad)) {
1453                setThirdLegEngineRoad(newRoad);
1454            }
1455            if (getThirdLegCabooseRoad().equals(oldRoad)) {
1456                setThirdLegCabooseRoad(newRoad);
1457            }
1458        }
1459    }
1460
1461    /**
1462     * Gets the car load option for this train.
1463     *
1464     * @return ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1465     */
1466    public String getLoadOption() {
1467        return _loadOption;
1468    }
1469
1470    /**
1471     * Set how this train deals with car loads
1472     *
1473     * @param option ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1474     */
1475    public void setLoadOption(String option) {
1476        String old = _loadOption;
1477        _loadOption = option;
1478        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, old, option);
1479    }
1480
1481    List<String> _loadList = new ArrayList<>();
1482
1483    public void setLoadNames(String[] loads) {
1484        if (loads.length > 0) {
1485            Arrays.sort(loads);
1486            for (String load : loads) {
1487                if (!load.isEmpty()) {
1488                    _loadList.add(load);
1489                }
1490            }
1491        }
1492    }
1493
1494    /**
1495     * Provides a list of loads that the train will either service or exclude.
1496     * See setLoadOption
1497     *
1498     * @return Array of load names as Strings
1499     */
1500    public String[] getLoadNames() {
1501        String[] loads = _loadList.toArray(new String[0]);
1502        if (_loadList.size() > 0) {
1503            Arrays.sort(loads);
1504        }
1505        return loads;
1506    }
1507
1508    /**
1509     * Add a load that the train will either service or exclude. See
1510     * setLoadOption
1511     *
1512     * @param load The string load name.
1513     * @return true if load name was added, false if load name wasn't in the
1514     *         list.
1515     */
1516    public boolean addLoadName(String load) {
1517        if (_loadList.contains(load)) {
1518            return false;
1519        }
1520        _loadList.add(load);
1521        log.debug("train ({}) add car load {}", getName(), load);
1522        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() - 1, _loadList.size());
1523        return true;
1524    }
1525
1526    /**
1527     * Delete a load name that the train will either service or exclude. See
1528     * setLoadOption
1529     *
1530     * @param load The string load name.
1531     * @return true if load name was removed, false if load name wasn't in the
1532     *         list.
1533     */
1534    public boolean deleteLoadName(String load) {
1535        if (_loadList.remove(load)) {
1536            log.debug("train ({}) delete car load {}", getName(), load);
1537            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() + 1, _loadList.size());
1538            return true;
1539        }
1540        return false;
1541    }
1542
1543    /**
1544     * Determine if train will service a specific load name.
1545     *
1546     * @param load the load name to check.
1547     * @return true if train will service this load.
1548     */
1549    public boolean isLoadNameAccepted(String load) {
1550        if (_loadOption.equals(ALL_LOADS)) {
1551            return true;
1552        }
1553        if (_loadOption.equals(INCLUDE_LOADS)) {
1554            return _loadList.contains(load);
1555        }
1556        // exclude!
1557        return !_loadList.contains(load);
1558    }
1559
1560    /**
1561     * Determine if train will service a specific load and car type.
1562     *
1563     * @param load the load name to check.
1564     * @param type the type of car used to carry the load.
1565     * @return true if train will service this load.
1566     */
1567    public boolean isLoadNameAccepted(String load, String type) {
1568        if (_loadOption.equals(ALL_LOADS)) {
1569            return true;
1570        }
1571        if (_loadOption.equals(INCLUDE_LOADS)) {
1572            return _loadList.contains(load) || _loadList.contains(type + CarLoad.SPLIT_CHAR + load);
1573        }
1574        // exclude!
1575        return !_loadList.contains(load) && !_loadList.contains(type + CarLoad.SPLIT_CHAR + load);
1576    }
1577
1578    public String getOwnerOption() {
1579        return _ownerOption;
1580    }
1581
1582    /**
1583     * Set how this train deals with car owner names
1584     *
1585     * @param option ALL_OWNERS INCLUDE_OWNERS EXCLUDE_OWNERS
1586     */
1587    public void setOwnerOption(String option) {
1588        String old = _ownerOption;
1589        _ownerOption = option;
1590        setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, old, option);
1591    }
1592
1593    List<String> _ownerList = new ArrayList<>();
1594
1595    public void setOwnerNames(String[] owners) {
1596        if (owners.length > 0) {
1597            Arrays.sort(owners);
1598            for (String owner : owners) {
1599                if (!owner.isEmpty()) {
1600                    _ownerList.add(owner);
1601                }
1602            }
1603        }
1604    }
1605
1606    /**
1607     * Provides a list of owner names that the train will either service or
1608     * exclude. See setOwnerOption
1609     *
1610     * @return Array of owner names as Strings
1611     */
1612    public String[] getOwnerNames() {
1613        String[] owners = _ownerList.toArray(new String[0]);
1614        if (_ownerList.size() > 0) {
1615            Arrays.sort(owners);
1616        }
1617        return owners;
1618    }
1619
1620    /**
1621     * Add a owner name that the train will either service or exclude. See
1622     * setOwnerOption
1623     *
1624     * @param owner The string representing the owner's name.
1625     * @return true if owner name was added, false if owner name wasn't in the
1626     *         list.
1627     */
1628    public boolean addOwnerName(String owner) {
1629        if (_ownerList.contains(owner)) {
1630            return false;
1631        }
1632        _ownerList.add(owner);
1633        log.debug("train ({}) add car owner {}", getName(), owner);
1634        setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, _ownerList.size() - 1, _ownerList.size());
1635        return true;
1636    }
1637
1638    /**
1639     * Delete a owner name that the train will either service or exclude. See
1640     * setOwnerOption
1641     *
1642     * @param owner The string representing the owner's name.
1643     * @return true if owner name was removed, false if owner name wasn't in the
1644     *         list.
1645     */
1646    public boolean deleteOwnerName(String owner) {
1647        if (_ownerList.remove(owner)) {
1648            log.debug("train ({}) delete car owner {}", getName(), owner);
1649            setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, _ownerList.size() + 1, _ownerList.size());
1650            return true;
1651        }
1652        return false;
1653    }
1654
1655    /**
1656     * Determine if train will service a specific owner name.
1657     *
1658     * @param owner the owner name to check.
1659     * @return true if train will service this owner name.
1660     */
1661    public boolean isOwnerNameAccepted(String owner) {
1662        if (_ownerOption.equals(ALL_OWNERS)) {
1663            return true;
1664        }
1665        if (_ownerOption.equals(INCLUDE_OWNERS)) {
1666            return _ownerList.contains(owner);
1667        }
1668        // exclude!
1669        return !_ownerList.contains(owner);
1670    }
1671
1672    protected void replaceOwner(String oldName, String newName) {
1673        if (deleteOwnerName(oldName)) {
1674            addOwnerName(newName);
1675        }
1676    }
1677
1678    /**
1679     * Only rolling stock built in or after this year will be used.
1680     *
1681     * @param year A string representing a year.
1682     */
1683    public void setBuiltStartYear(String year) {
1684        String old = _builtStartYear;
1685        _builtStartYear = year;
1686        if (!old.equals(year)) {
1687            setDirtyAndFirePropertyChange(BUILT_YEAR_CHANGED_PROPERTY, old, year);
1688        }
1689    }
1690
1691    public String getBuiltStartYear() {
1692        return _builtStartYear;
1693    }
1694
1695    /**
1696     * Only rolling stock built in or before this year will be used.
1697     *
1698     * @param year A string representing a year.
1699     */
1700    public void setBuiltEndYear(String year) {
1701        String old = _builtEndYear;
1702        _builtEndYear = year;
1703        if (!old.equals(year)) {
1704            setDirtyAndFirePropertyChange(BUILT_YEAR_CHANGED_PROPERTY, old, year);
1705        }
1706    }
1707
1708    public String getBuiltEndYear() {
1709        return _builtEndYear;
1710    }
1711
1712    /**
1713     * Determine if train will service rolling stock by built date.
1714     *
1715     * @param date A string representing the built date for a car or engine.
1716     * @return true is built date is in the acceptable range.
1717     */
1718    public boolean isBuiltDateAccepted(String date) {
1719        if (getBuiltStartYear().equals(NONE) && getBuiltEndYear().equals(NONE)) {
1720            return true; // range dates not defined
1721        }
1722        int startYear = 0; // default start year;
1723        int endYear = 99999; // default end year;
1724        int builtYear = -1900;
1725        if (!getBuiltStartYear().equals(NONE)) {
1726            try {
1727                startYear = Integer.parseInt(getBuiltStartYear());
1728            } catch (NumberFormatException e) {
1729                log.debug("Train ({}) built start date not initialized, start: {}", getName(), getBuiltStartYear());
1730            }
1731        }
1732        if (!getBuiltEndYear().equals(NONE)) {
1733            try {
1734                endYear = Integer.parseInt(getBuiltEndYear());
1735            } catch (NumberFormatException e) {
1736                log.debug("Train ({}) built end date not initialized, end: {}", getName(), getBuiltEndYear());
1737            }
1738        }
1739        try {
1740            builtYear = Integer.parseInt(RollingStockManager.convertBuildDate(date));
1741        } catch (NumberFormatException e) {
1742            log.debug("Unable to parse car built date {}", date);
1743        }
1744        if (startYear < builtYear && builtYear < endYear) {
1745            return true;
1746        }
1747        return false;
1748    }
1749
1750    private final boolean debugFlag = false;
1751
1752    /**
1753     * Determines if this train will service this car. Note this code doesn't
1754     * check the location or tracks that needs to be done separately. See
1755     * Router.java.
1756     *
1757     * @param car The car to be tested.
1758     * @return true if this train can service the car.
1759     */
1760    public boolean isServiceable(Car car) {
1761        return isServiceable(null, car);
1762    }
1763
1764    /**
1765     * Note that this code was written after TrainBuilder. It does pretty much
1766     * the same as TrainBuilder but with much fewer build report messages.
1767     *
1768     * @param buildReport PrintWriter
1769     * @param car         the car to be tested
1770     * @return true if this train can service the car.
1771     */
1772    public boolean isServiceable(PrintWriter buildReport, Car car) {
1773        setServiceStatus(NONE);
1774        // check to see if train can carry car
1775        if (!isTrainAbleToService(buildReport, car)) {
1776            return false;
1777        }
1778
1779        Route route = getRoute();
1780        if (route == null) {
1781            return false;
1782        }
1783
1784        if (car.getLocation() == null || car.getTrack() == null) {
1785            return false;
1786        }
1787
1788        // determine if the car's location is serviced by this train
1789        if (route.getLastLocationByName(car.getLocationName()) == null) {
1790            addLine(buildReport, Bundle.getMessage("trainNotThisLocation",
1791                    getName(), car.getLocationName()));
1792            return false;
1793        }
1794        // determine if the car's destination is serviced by this train
1795        // check to see if destination is staging and is also the last location in the train's route
1796        if (car.getDestination() != null &&
1797                (route.getLastLocationByName(car.getDestinationName()) == null ||
1798                        (car.getDestination().isStaging() &&
1799                                getTrainTerminatesRouteLocation().getLocation() != car.getDestination()))) {
1800            addLine(buildReport, Bundle.getMessage("trainNotThisLocation",
1801                    getName(), car.getDestinationName()));
1802            return false;
1803        }
1804        // now find the car in the train's route
1805        List<RouteLocation> rLocations = route.getLocationsBySequenceList();
1806        for (RouteLocation rLoc : rLocations) {
1807            if (rLoc.getName().equals(car.getLocationName())) {
1808                if (rLoc.getMaxCarMoves() <= 0 ||
1809                        isLocationSkipped(rLoc) ||
1810                        !rLoc.isPickUpAllowed() && !car.isLocalMove() ||
1811                        !rLoc.isLocalMovesAllowed() && car.isLocalMove()) {
1812                    addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarFrom",
1813                            getName(), car.toString(), car.getLocationName(), car.getTrackName(), rLoc.getId()));
1814                    continue;
1815                }
1816                // check train and car's location direction
1817                if ((car.getLocation().getTrainDirections() & rLoc.getTrainDirection()) == 0 && !isLocalSwitcher()) {
1818                    addLine(buildReport,
1819                            Bundle.getMessage("trainCanNotServiceCarLocation",
1820                                    getName(), car.toString(), car.getLocationName(), car.getTrackName(),
1821                                    rLoc.getId(), car.getLocationName(), rLoc.getTrainDirectionString()));
1822                    continue;
1823                }
1824                // check train and car's track direction
1825                if ((car.getTrack().getTrainDirections() & rLoc.getTrainDirection()) == 0 && !isLocalSwitcher()) {
1826                    addLine(buildReport,
1827                            Bundle.getMessage("trainCanNotServiceCarTrack",
1828                                    getName(), car.toString(), car.getLocationName(), car.getTrackName(),
1829                                    rLoc.getId(), car.getTrackName(), rLoc.getTrainDirectionString()));
1830                    continue;
1831                }
1832                // can train pull this car?
1833                if (!car.getTrack().isPickupTrainAccepted(this)) {
1834                    addLine(buildReport,
1835                            Bundle.getMessage("trainCanNotServiceCarPickup",
1836                                    getName(), car.toString(), car.getLocationName(), car.getTrackName(),
1837                                    rLoc.getId(), car.getTrackName(), getName()));
1838                    continue;
1839                }
1840                if (debugFlag) {
1841                    log.debug("Car ({}) can be picked up by train ({}) location ({}, {}) destination ({}, {})",
1842                            car.toString(), getName(), car.getLocationName(), car.getTrackName(),
1843                            car.getDestinationName(), car.getDestinationTrackName());
1844                }
1845                addLine(buildReport, Bundle.getMessage("trainCanPickUpCar",
1846                        getName(), car.toString(), car.getLocationName(), car.getTrackName(), rLoc.getId()));
1847                if (car.getDestination() == null) {
1848                    if (debugFlag) {
1849                        log.debug("Car ({}) does not have a destination", car.toString());
1850                    }
1851                    return true; // done
1852                }
1853                // now check car's destination
1854                if (isServiceableDestination(buildReport, car, rLoc, rLocations)) {
1855                    return true; // train can carry car
1856                }
1857                continue; // maybe another pick up point in the route?
1858            }
1859        }
1860        if (debugFlag) {
1861            log.debug("Train ({}) can't service car ({}) from ({}, {})", getName(), car.toString(),
1862                    car.getLocationName(), car.getTrackName());
1863        }
1864        return false;
1865    }
1866
1867    /**
1868     * Second step in determining if train can service car, check to see if
1869     * car's destination is serviced by this train's route.
1870     *
1871     * @param buildReport add messages if needed to build report
1872     * @param car         The test car
1873     * @param rLoc        Where in the train's route the car was found
1874     * @param rLocations  The ordered routeLocations in this train's route
1875     * @return true if car's destination can be serviced
1876     */
1877    private boolean isServiceableDestination(PrintWriter buildReport, Car car, RouteLocation rLoc,
1878            List<RouteLocation> rLocations) {
1879        // car can be a kernel so get total length
1880        int length = car.getTotalKernelLength();
1881        // now see if the train's route services the car's destination
1882        for (int k = rLocations.indexOf(rLoc); k < rLocations.size(); k++) {
1883            RouteLocation rldest = rLocations.get(k);
1884            car.setRouteDestinationTiming(rldest);
1885            if (rldest.getName().equals(car.getDestinationName()) &&
1886                    (rldest.isDropAllowed() && !car.isLocalMove() ||
1887                            rldest.isLocalMovesAllowed() && car.isLocalMove()) &&
1888                    rldest.getMaxCarMoves() > 0 &&
1889                    !isLocationSkipped(rldest) &&
1890                    (!Setup.isCheckCarDestinationEnabled() ||
1891                            car.getTrack().isDestinationAccepted(car.getDestination()))) {
1892                // found the car's destination
1893                // check track and train direction
1894                if ((car.getDestination().getTrainDirections() & rldest.getTrainDirection()) == 0 &&
1895                        !isLocalSwitcher()) {
1896                    addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarDestination",
1897                            getName(), car.toString(), car.getDestinationName(), rldest.getId(),
1898                            rldest.getTrainDirectionString()));
1899                    continue;
1900                }
1901                //check destination track
1902                if (car.getDestinationTrack() != null) {
1903                    if (!isServicableTrack(buildReport, car, rldest, car.getDestinationTrack())) {
1904                        continue;
1905                    }
1906                    // car doesn't have a destination track
1907                    // car going to staging?
1908                } else if (!isCarToStaging(buildReport, rldest, car)) {
1909                    continue;
1910                } else {
1911                    if (debugFlag) {
1912                        log.debug("Find track for car ({}) at destination ({})", car.toString(),
1913                                car.getDestinationName());
1914                    }
1915                    // determine if there's a destination track that is willing to accept this car
1916                    String status = "";
1917                    List<Track> tracks = rldest.getLocation().getTracksList();
1918                    for (Track track : tracks) {
1919                        if (!isServicableTrack(buildReport, car, rldest, track)) {
1920                            continue;
1921                        }
1922                        // will the track accept this car?
1923                        status = track.isRollingStockAccepted(car);
1924                        if (status.equals(Track.OKAY) || status.startsWith(Track.LENGTH)) {
1925                            if (debugFlag) {
1926                                log.debug("Found track ({}) for car ({})", track.getName(), car.toString());
1927                            }
1928                            break; // found track
1929                        }
1930                    }
1931                    if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
1932                        if (debugFlag) {
1933                            log.debug("Destination ({}) can not service car ({}) using train ({}) no track available",
1934                                    car.getDestinationName(), car.toString(), getName()); // NOI18N
1935                        }
1936                        addLine(buildReport, Bundle.getMessage("trainCanNotDeliverNoTracks",
1937                                getName(), car.toString(), car.getDestinationName(), rldest.getId()));
1938                        continue;
1939                    }
1940                }
1941                // restriction to only carry cars to terminal?
1942                if (!isOnlyToTerminal(buildReport, car)) {
1943                    continue;
1944                }
1945                // don't allow local move when car is in staging
1946                if (!isTurn() &&
1947                        car.getTrack().isStaging() &&
1948                        rldest.getLocation() == car.getLocation()) {
1949                    log.debug(
1950                            "Car ({}) at ({}, {}) not allowed to perform local move in staging ({})",
1951                            car.toString(), car.getLocationName(), car.getTrackName(), rldest.getName());
1952                    continue;
1953                }
1954                // allow car to return to staging?
1955                if (isAllowReturnToStagingEnabled() &&
1956                        car.getTrack().isStaging() &&
1957                        rldest.getLocation() == car.getLocation()) {
1958                    addLine(buildReport,
1959                            Bundle.getMessage("trainCanReturnCarToStaging",
1960                                    getName(), car.toString(), car.getDestinationName(),
1961                                    car.getDestinationTrackName()));
1962                    return true; // done
1963                }
1964                // is this local move allowed?
1965                if (!isLocalMoveAllowed(buildReport, car, rLoc, rldest)) {
1966                    continue;
1967                }
1968                // Can cars travel from origin to terminal?
1969                if (!isTravelOriginToTerminalAllowed(buildReport, rLoc, rldest, car)) {
1970                    continue;
1971                }
1972                // check to see if moves are available
1973                if (!isRouteMovesAvailable(buildReport, rldest)) {
1974                    continue;
1975                }
1976                if (debugFlag) {
1977                    log.debug("Car ({}) can be dropped by train ({}) to ({}, {})", car.toString(), getName(),
1978                            car.getDestinationName(), car.getDestinationTrackName());
1979                }
1980                return true; // done
1981            }
1982            // check to see if train length is okay
1983            if (!isTrainLengthOkay(buildReport, car, rldest, length)) {
1984                return false;
1985            }
1986        }
1987        addLine(buildReport, Bundle.getMessage("trainCanNotDeliverToDestination",
1988                getName(), car.toString(), car.getDestinationName(), car.getDestinationTrackName()));
1989        return false;
1990    }
1991    
1992    public boolean isTrainAbleToService(PrintWriter buildReport, Car car) {
1993        if (!isTypeNameAccepted(car.getTypeName())) {
1994            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarType",
1995                    getName(), car.toString(), car.getTypeName()));
1996            return false;
1997        }
1998        if (!isLoadNameAccepted(car.getLoadName(), car.getTypeName())) {
1999            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarLoad",
2000                    getName(), car.toString(), car.getTypeName(), car.getLoadName()));
2001            return false;
2002        }
2003        if (!isBuiltDateAccepted(car.getBuilt()) ||
2004                !isOwnerNameAccepted(car.getOwnerName()) ||
2005                (!car.isCaboose() && !isCarRoadNameAccepted(car.getRoadName())) ||
2006                (car.isCaboose() && !isCabooseRoadNameAccepted(car.getRoadName()))) {
2007            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCar",
2008                    getName(), car.toString()));
2009            return false;
2010        }
2011        return true;
2012    }
2013
2014    private boolean isServicableTrack(PrintWriter buildReport, Car car, RouteLocation rldest, Track track) {
2015        // train and track direction
2016        if ((track.getTrainDirections() & rldest.getTrainDirection()) == 0 && !isLocalSwitcher()) {
2017            addLine(buildReport, Bundle.getMessage("buildCanNotDropRsUsingTrain",
2018                    car.toString(), rldest.getTrainDirectionString(), track.getName()));
2019            return false;
2020        }
2021        if (!track.isDropTrainAccepted(this)) {
2022            addLine(buildReport, Bundle.getMessage("buildCanNotDropTrain",
2023                    car.toString(), getName(), track.getTrackTypeName(), track.getLocation().getName(),
2024                    track.getName()));
2025            return false;
2026        }
2027        return true;
2028    }
2029
2030    private boolean isCarToStaging(PrintWriter buildReport, RouteLocation rldest, Car car) {
2031        if (rldest.getLocation().isStaging() &&
2032                isBuilding() &&
2033                getTerminationTrack() != null &&
2034                getTerminationTrack().getLocation() == rldest.getLocation()) {
2035            if (debugFlag) {
2036                log.debug("Car ({}) destination is staging, check train ({}) termination track ({})",
2037                        car.toString(), getName(), getTerminationTrack().getName());
2038            }
2039            String status = car.checkDestination(getTerminationTrack().getLocation(), getTerminationTrack());
2040            if (!status.equals(Track.OKAY)) {
2041                addLine(buildReport,
2042                        Bundle.getMessage("trainCanNotDeliverToStaging",
2043                                getName(), car.toString(),
2044                                getTerminationTrack().getLocation().getName(),
2045                                getTerminationTrack().getName(), status));
2046                setServiceStatus(status);
2047                return false;
2048            }
2049        }
2050        return true;
2051    }
2052
2053    private boolean isOnlyToTerminal(PrintWriter buildReport, Car car) {
2054        // ignore send to terminal if a local move
2055        if (isSendCarsToTerminalEnabled() &&
2056                !car.isLocalMove() &&
2057                !car.getSplitLocationName()
2058                        .equals(TrainCommon.splitString(getTrainDepartsName())) &&
2059                !car.getSplitDestinationName()
2060                        .equals(TrainCommon.splitString(getTrainTerminatesName()))) {
2061            if (debugFlag) {
2062                log.debug("option send cars to terminal is enabled");
2063            }
2064            addLine(buildReport,
2065                    Bundle.getMessage("trainCanNotCarryCarOption",
2066                            getName(), car.toString(), car.getLocationName(),
2067                            car.getTrackName(), car.getDestinationName(),
2068                            car.getDestinationTrackName()));
2069            return false;
2070        }
2071        return true;
2072    }
2073
2074    private boolean isLocalMoveAllowed(PrintWriter buildReport, Car car, RouteLocation rLoc, RouteLocation rldest) {
2075        if ((!isAllowLocalMovesEnabled() || !rLoc.isLocalMovesAllowed() || !rldest.isLocalMovesAllowed()) &&
2076                !isLocalSwitcher() &&
2077                !car.isCaboose() &&
2078                !car.hasFred() &&
2079                !car.isPassenger() &&
2080                car.isLocalMove()) {
2081            if (debugFlag) {
2082                log.debug("Local move not allowed");
2083            }
2084            addLine(buildReport, Bundle.getMessage("trainCanNotPerformLocalMove",
2085                    getName(), car.toString(), car.getLocationName()));
2086            return false;
2087        }
2088        return true;
2089    }
2090
2091    private boolean isTravelOriginToTerminalAllowed(PrintWriter buildReport, RouteLocation rLoc, RouteLocation rldest,
2092            Car car) {
2093        if (!isAllowThroughCarsEnabled() &&
2094                TrainCommon.splitString(getTrainDepartsName())
2095                        .equals(rLoc.getSplitName()) &&
2096                TrainCommon.splitString(getTrainTerminatesName())
2097                        .equals(rldest.getSplitName()) &&
2098                !TrainCommon.splitString(getTrainDepartsName())
2099                        .equals(TrainCommon.splitString(getTrainTerminatesName())) &&
2100                !isLocalSwitcher() &&
2101                !car.isCaboose() &&
2102                !car.hasFred() &&
2103                !car.isPassenger()) {
2104            if (debugFlag) {
2105                log.debug("Through car ({}) not allowed", car.toString());
2106            }
2107            addLine(buildReport, Bundle.getMessage("trainDoesNotCarryOriginTerminal",
2108                    getName(), car.getLocationName(), car.getDestinationName()));
2109            return false;
2110        }
2111        return true;
2112    }
2113
2114    private boolean isRouteMovesAvailable(PrintWriter buildReport, RouteLocation rldest) {
2115        if (isBuilding() && rldest.getMaxCarMoves() - rldest.getCarMoves() <= 0) {
2116            setServiceStatus(Bundle.getMessage("trainNoMoves",
2117                    getName(), getRoute().getName(), rldest.getId(), rldest.getName()));
2118            if (debugFlag) {
2119                log.debug("No available moves for destination {}", rldest.getName());
2120            }
2121            addLine(buildReport, getServiceStatus());
2122            return false;
2123        }
2124        return true;
2125    }
2126
2127    private boolean isTrainLengthOkay(PrintWriter buildReport, Car car, RouteLocation rldest, int length) {
2128        if (isBuilding() && rldest.getTrainLength() + length > rldest.getMaxTrainLength()) {
2129            setServiceStatus(Bundle.getMessage("trainExceedsMaximumLength",
2130                    getName(), getRoute().getName(), rldest.getId(), rldest.getMaxTrainLength(),
2131                    Setup.getLengthUnit().toLowerCase(), rldest.getName(), car.toString(),
2132                    rldest.getTrainLength() + length - rldest.getMaxTrainLength()));
2133            if (debugFlag) {
2134                log.debug("Car ({}) exceeds maximum train length {} when departing ({})", car.toString(),
2135                        rldest.getMaxTrainLength(), rldest.getName());
2136            }
2137            addLine(buildReport, getServiceStatus());
2138            return false;
2139        }
2140        return true;
2141    }
2142
2143    protected static final String SEVEN = Setup.BUILD_REPORT_VERY_DETAILED;
2144
2145    private void addLine(PrintWriter buildReport, String string) {
2146        if (Setup.getRouterBuildReportLevel().equals(SEVEN)) {
2147            TrainCommon.addLine(buildReport, SEVEN, string);
2148        }
2149    }
2150
2151    protected void setServiceStatus(String status) {
2152        _serviceStatus = status;
2153    }
2154
2155    /**
2156     * Returns the statusCode of the "isServiceable(Car)" routine. There are two
2157     * statusCodes that need special consideration when the train is being
2158     * built, the moves in a train's route and the maximum train length. NOTE:
2159     * The code using getServiceStatus() currently assumes that if there's a
2160     * service status that the issue is either route moves or maximum train
2161     * length.
2162     *
2163     * @return The statusCode.
2164     */
2165    public String getServiceStatus() {
2166        return _serviceStatus;
2167    }
2168
2169    /**
2170     * @return The number of cars worked by this train
2171     */
2172    public int getNumberCarsWorked() {
2173        int count = 0;
2174        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
2175            if (rs.getRouteLocation() != null) {
2176                count++;
2177            }
2178        }
2179        return count;
2180    }
2181
2182    public void setNumberCarsRequested(int number) {
2183        _statusCarsRequested = number;
2184    }
2185
2186    public int getNumberCarsRequested() {
2187        return _statusCarsRequested;
2188    }
2189
2190    public void setDate(LocalDateTime date) {
2191        _date = date;
2192    }
2193
2194    public String getSortDate() {
2195        if (_date == null) {
2196            return NONE;
2197        }
2198        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); // NOI18N
2199        return _date.format(format);
2200    }
2201
2202    public String getDate() {
2203        if (_date == null) {
2204            return NONE;
2205        }
2206        DateTimeFormatter format = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); // NOI18N
2207        return _date.format(format);
2208    }
2209
2210    /**
2211     * Gets the number of cars in the train at the current location in the
2212     * train's route.
2213     *
2214     * @return The number of cars currently in the train
2215     */
2216    public int getNumberCarsInTrain() {
2217        return getNumberCarsInTrain(getCurrentRouteLocation());
2218    }
2219
2220    /**
2221     * Gets the number of cars in the train when train departs the route
2222     * location.
2223     *
2224     * @param routeLocation The RouteLocation.
2225     * @return The number of cars in the train departing the route location.
2226     */
2227    public int getNumberCarsInTrain(RouteLocation routeLocation) {
2228        int number = 0;
2229        Route route = getRoute();
2230        if (route != null) {
2231            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2232                for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
2233                    if (rs.getRouteLocation() == rl) {
2234                        number++;
2235                    }
2236                    if (rs.getRouteDestination() == rl) {
2237                        number--;
2238                    }
2239                }
2240                if (rl == routeLocation) {
2241                    break;
2242                }
2243            }
2244        }
2245        return number;
2246    }
2247
2248    /**
2249     * Gets the number of empty cars in the train when train departs the route
2250     * location.
2251     *
2252     * @param routeLocation The RouteLocation.
2253     * @return The number of empty cars in the train departing the route
2254     *         location.
2255     */
2256    public int getNumberEmptyCarsInTrain(RouteLocation routeLocation) {
2257        int number = 0;
2258        Route route = getRoute();
2259        if (route != null) {
2260            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2261                for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
2262                    if (!car.getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
2263                        continue;
2264                    }
2265                    if (car.getRouteLocation() == rl) {
2266                        number++;
2267                    }
2268                    if (car.getRouteDestination() == rl) {
2269                        number--;
2270                    }
2271                }
2272                if (rl == routeLocation) {
2273                    break;
2274                }
2275            }
2276        }
2277
2278        return number;
2279    }
2280
2281    public int getNumberLoadedCarsInTrain(RouteLocation routeLocation) {
2282        return getNumberCarsInTrain(routeLocation) - getNumberEmptyCarsInTrain(routeLocation);
2283    }
2284
2285    public int getNumberCarsPickedUp() {
2286        return getNumberCarsPickedUp(getCurrentRouteLocation());
2287    }
2288
2289    /**
2290     * Gets the number of cars pulled from a location
2291     *
2292     * @param routeLocation the location
2293     * @return number of pick ups
2294     */
2295    public int getNumberCarsPickedUp(RouteLocation routeLocation) {
2296        int number = 0;
2297        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
2298            if (rs.getRouteLocation() == routeLocation && rs.getTrack() != null) {
2299                number++;
2300            }
2301        }
2302        return number;
2303    }
2304
2305    public int getNumberCarsSetout() {
2306        return getNumberCarsSetout(getCurrentRouteLocation());
2307    }
2308
2309    /**
2310     * Gets the number of cars delivered to a location
2311     *
2312     * @param routeLocation the location
2313     * @return number of set outs
2314     */
2315    public int getNumberCarsSetout(RouteLocation routeLocation) {
2316        int number = 0;
2317        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
2318            if (rs.getRouteDestination() == routeLocation) {
2319                number++;
2320            }
2321        }
2322        return number;
2323    }
2324
2325    /**
2326     * Gets the train's length at the current location in the train's route.
2327     *
2328     * @return The train length at the train's current location
2329     */
2330    public int getTrainLength() {
2331        return getTrainLength(getCurrentRouteLocation());
2332    }
2333
2334    /**
2335     * Gets the train's length at the route location specified
2336     *
2337     * @param routeLocation The route location
2338     * @return The train length at the route location
2339     */
2340    public int getTrainLength(RouteLocation routeLocation) {
2341        int length = 0;
2342        Route route = getRoute();
2343        if (route != null) {
2344            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2345                for (RollingStock rs : InstanceManager.getDefault(EngineManager.class).getList(this)) {
2346                    if (rs.getRouteLocation() == rl) {
2347                        length += rs.getTotalLength();
2348                    }
2349                    if (rs.getRouteDestination() == rl) {
2350                        length += -rs.getTotalLength();
2351                    }
2352                }
2353                for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
2354                    if (rs.getRouteLocation() == rl) {
2355                        length += rs.getTotalLength();
2356                    }
2357                    if (rs.getRouteDestination() == rl) {
2358                        length += -rs.getTotalLength();
2359                    }
2360                }
2361                if (rl == routeLocation) {
2362                    break;
2363                }
2364            }
2365        }
2366        return length;
2367    }
2368
2369    /**
2370     * Get the train's weight at the current location.
2371     *
2372     * @return Train's weight in tons.
2373     */
2374    public int getTrainWeight() {
2375        return getTrainWeight(getCurrentRouteLocation());
2376    }
2377
2378    public int getTrainWeight(RouteLocation routeLocation) {
2379        int weight = 0;
2380        Route route = getRoute();
2381        if (route != null) {
2382            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2383                for (RollingStock rs : InstanceManager.getDefault(EngineManager.class).getList(this)) {
2384                    if (rs.getRouteLocation() == rl) {
2385                        weight += rs.getAdjustedWeightTons();
2386                    }
2387                    if (rs.getRouteDestination() == rl) {
2388                        weight += -rs.getAdjustedWeightTons();
2389                    }
2390                }
2391                for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
2392                    if (car.getRouteLocation() == rl) {
2393                        weight += car.getAdjustedWeightTons(); // weight depends
2394                                                               // on car load
2395                    }
2396                    if (car.getRouteDestination() == rl) {
2397                        weight += -car.getAdjustedWeightTons();
2398                    }
2399                }
2400                if (rl == routeLocation) {
2401                    break;
2402                }
2403            }
2404        }
2405        return weight;
2406    }
2407
2408    /**
2409     * Gets the train's locomotive horsepower at the route location specified
2410     *
2411     * @param routeLocation The route location
2412     * @return The train's locomotive horsepower at the route location
2413     */
2414    public int getTrainHorsePower(RouteLocation routeLocation) {
2415        int hp = 0;
2416        Route route = getRoute();
2417        if (route != null) {
2418            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2419                for (Engine eng : InstanceManager.getDefault(EngineManager.class).getList(this)) {
2420                    if (eng.getRouteLocation() == rl) {
2421                        hp += eng.getHpInteger();
2422                    }
2423                    if (eng.getRouteDestination() == rl) {
2424                        hp += -eng.getHpInteger();
2425                    }
2426                }
2427                if (rl == routeLocation) {
2428                    break;
2429                }
2430            }
2431        }
2432        return hp;
2433    }
2434    
2435    public int getNumberEngines(RouteLocation routeLocation) {
2436        int numberEngines = 0;
2437        Route route = getRoute();
2438        if (route != null) {
2439            for (RouteLocation rl : route.getLocationsBySequenceList()) {
2440                for (Engine eng : InstanceManager.getDefault(EngineManager.class).getList(this)) {
2441                    if (eng.getRouteLocation() == rl) {
2442                        numberEngines++;
2443                    }
2444                    if (eng.getRouteDestination() == rl) {
2445                        numberEngines--;
2446                    }
2447                }
2448                if (rl == routeLocation) {
2449                    break;
2450                }
2451            }
2452        }
2453        
2454        
2455        return numberEngines;
2456    }
2457
2458    /**
2459     * Gets the current caboose road and number if there's one assigned to the
2460     * train.
2461     *
2462     * @return Road and number of caboose.
2463     */
2464    public String getCabooseRoadAndNumber() {
2465        String cabooseRoadNumber = NONE;
2466        RouteLocation rl = getCurrentRouteLocation();
2467        List<Car> cars = InstanceManager.getDefault(CarManager.class).getByTrainList(this);
2468        for (Car car : cars) {
2469            if (car.getRouteLocation() == rl && car.isCaboose()) {
2470                cabooseRoadNumber =
2471                        car.getRoadName().split(TrainCommon.HYPHEN)[0] + " " + TrainCommon.splitString(car.getNumber());
2472            }
2473        }
2474        return cabooseRoadNumber;
2475    }
2476
2477    public void setDescription(String description) {
2478        String old = _description;
2479        _description = description;
2480        if (!old.equals(description)) {
2481            setDirtyAndFirePropertyChange(DESCRIPTION_CHANGED_PROPERTY, old, description);
2482        }
2483    }
2484
2485    public String getRawDescription() {
2486        return _description;
2487    }
2488
2489    /**
2490     * Returns a formated string providing the train's description. {0} = lead
2491     * engine number, {1} = train's departure direction {2} = lead engine road
2492     * {3} = DCC address of lead engine.
2493     *
2494     * @return The train's description.
2495     */
2496    public String getDescription() {
2497        try {
2498            String description = MessageFormat.format(getRawDescription(), new Object[]{getLeadEngineNumber(),
2499                    getTrainDepartsDirection(), getLeadEngineRoadName(), getLeadEngineDccAddress()});
2500            return description;
2501        } catch (IllegalArgumentException e) {
2502            return "ERROR IN FORMATTING: " + getRawDescription();
2503        }
2504    }
2505
2506    public void setNumberEngines(String number) {
2507        String old = _numberEngines;
2508        _numberEngines = number;
2509        if (!old.equals(number)) {
2510            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
2511        }
2512    }
2513
2514    /**
2515     * Get the number of engines that this train requires.
2516     *
2517     * @return The number of engines that this train requires.
2518     */
2519    public String getNumberEngines() {
2520        return _numberEngines;
2521    }
2522
2523    /**
2524     * Get the number of engines needed for the second set.
2525     *
2526     * @return The number of engines needed in route
2527     */
2528    public String getSecondLegNumberEngines() {
2529        return _leg2Engines;
2530    }
2531
2532    public void setSecondLegNumberEngines(String number) {
2533        String old = _leg2Engines;
2534        _leg2Engines = number;
2535        if (!old.equals(number)) {
2536            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
2537        }
2538    }
2539
2540    /**
2541     * Get the number of engines needed for the third set.
2542     *
2543     * @return The number of engines needed in route
2544     */
2545    public String getThirdLegNumberEngines() {
2546        return _leg3Engines;
2547    }
2548
2549    public void setThirdLegNumberEngines(String number) {
2550        String old = _leg3Engines;
2551        _leg3Engines = number;
2552        if (!old.equals(number)) {
2553            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
2554        }
2555    }
2556
2557    /**
2558     * Set the road name of engines servicing this train.
2559     *
2560     * @param road The road name of engines servicing this train.
2561     */
2562    public void setEngineRoad(String road) {
2563        String old = _engineRoad;
2564        _engineRoad = road;
2565        if (!old.equals(road)) {
2566            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
2567        }
2568    }
2569
2570    /**
2571     * Get the road name of engines servicing this train.
2572     *
2573     * @return The road name of engines servicing this train.
2574     */
2575    public String getEngineRoad() {
2576        return _engineRoad;
2577    }
2578
2579    /**
2580     * Set the road name of engines servicing this train 2nd leg.
2581     *
2582     * @param road The road name of engines servicing this train.
2583     */
2584    public void setSecondLegEngineRoad(String road) {
2585        String old = _leg2Road;
2586        _leg2Road = road;
2587        if (!old.equals(road)) {
2588            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
2589        }
2590    }
2591
2592    /**
2593     * Get the road name of engines servicing this train 2nd leg.
2594     *
2595     * @return The road name of engines servicing this train.
2596     */
2597    public String getSecondLegEngineRoad() {
2598        return _leg2Road;
2599    }
2600
2601    /**
2602     * Set the road name of engines servicing this train 3rd leg.
2603     *
2604     * @param road The road name of engines servicing this train.
2605     */
2606    public void setThirdLegEngineRoad(String road) {
2607        String old = _leg3Road;
2608        _leg3Road = road;
2609        if (!old.equals(road)) {
2610            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
2611        }
2612    }
2613
2614    /**
2615     * Get the road name of engines servicing this train 3rd leg.
2616     *
2617     * @return The road name of engines servicing this train.
2618     */
2619    public String getThirdLegEngineRoad() {
2620        return _leg3Road;
2621    }
2622
2623    /**
2624     * Set the model name of engines servicing this train.
2625     *
2626     * @param model The model name of engines servicing this train.
2627     */
2628    public void setEngineModel(String model) {
2629        String old = _engineModel;
2630        _engineModel = model;
2631        if (!old.equals(model)) {
2632            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
2633        }
2634    }
2635
2636    public String getEngineModel() {
2637        return _engineModel;
2638    }
2639
2640    /**
2641     * Set the model name of engines servicing this train's 2nd leg.
2642     *
2643     * @param model The model name of engines servicing this train.
2644     */
2645    public void setSecondLegEngineModel(String model) {
2646        String old = _leg2Model;
2647        _leg2Model = model;
2648        if (!old.equals(model)) {
2649            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
2650        }
2651    }
2652
2653    public String getSecondLegEngineModel() {
2654        return _leg2Model;
2655    }
2656
2657    /**
2658     * Set the model name of engines servicing this train's 3rd leg.
2659     *
2660     * @param model The model name of engines servicing this train.
2661     */
2662    public void setThirdLegEngineModel(String model) {
2663        String old = _leg3Model;
2664        _leg3Model = model;
2665        if (!old.equals(model)) {
2666            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
2667        }
2668    }
2669
2670    public String getThirdLegEngineModel() {
2671        return _leg3Model;
2672    }
2673
2674    protected void replaceModel(String oldModel, String newModel) {
2675        if (getEngineModel().equals(oldModel)) {
2676            setEngineModel(newModel);
2677        }
2678        if (getSecondLegEngineModel().equals(oldModel)) {
2679            setSecondLegEngineModel(newModel);
2680        }
2681        if (getThirdLegEngineModel().equals(oldModel)) {
2682            setThirdLegEngineModel(newModel);
2683        }
2684    }
2685
2686    /**
2687     * Set the road name of the caboose servicing this train.
2688     *
2689     * @param road The road name of the caboose servicing this train.
2690     */
2691    public void setCabooseRoad(String road) {
2692        String old = _cabooseRoad;
2693        _cabooseRoad = road;
2694        if (!old.equals(road)) {
2695            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
2696        }
2697    }
2698
2699    public String getCabooseRoad() {
2700        return _cabooseRoad;
2701    }
2702
2703    /**
2704     * Set the road name of the second leg caboose servicing this train.
2705     *
2706     * @param road The road name of the caboose servicing this train's 2nd leg.
2707     */
2708    public void setSecondLegCabooseRoad(String road) {
2709        String old = _leg2CabooseRoad;
2710        _leg2CabooseRoad = road;
2711        if (!old.equals(road)) {
2712            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
2713        }
2714    }
2715
2716    public String getSecondLegCabooseRoad() {
2717        return _leg2CabooseRoad;
2718    }
2719
2720    /**
2721     * Set the road name of the third leg caboose servicing this train.
2722     *
2723     * @param road The road name of the caboose servicing this train's 3rd leg.
2724     */
2725    public void setThirdLegCabooseRoad(String road) {
2726        String old = _leg3CabooseRoad;
2727        _leg3CabooseRoad = road;
2728        if (!old.equals(road)) {
2729            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
2730        }
2731    }
2732
2733    public String getThirdLegCabooseRoad() {
2734        return _leg3CabooseRoad;
2735    }
2736
2737    public void setSecondLegStartRouteLocation(RouteLocation rl) {
2738        _leg2Start = rl;
2739    }
2740
2741    public RouteLocation getSecondLegStartRouteLocation() {
2742        return _leg2Start;
2743    }
2744
2745    public String getSecondLegStartLocationName() {
2746        if (getSecondLegStartRouteLocation() == null) {
2747            return NONE;
2748        }
2749        return getSecondLegStartRouteLocation().getName();
2750    }
2751
2752    public void setThirdLegStartRouteLocation(RouteLocation rl) {
2753        _leg3Start = rl;
2754    }
2755
2756    public RouteLocation getThirdLegStartRouteLocation() {
2757        return _leg3Start;
2758    }
2759
2760    public String getThirdLegStartLocationName() {
2761        if (getThirdLegStartRouteLocation() == null) {
2762            return NONE;
2763        }
2764        return getThirdLegStartRouteLocation().getName();
2765    }
2766
2767    public void setSecondLegEndRouteLocation(RouteLocation rl) {
2768        _end2Leg = rl;
2769    }
2770
2771    public String getSecondLegEndLocationName() {
2772        if (getSecondLegEndRouteLocation() == null) {
2773            return NONE;
2774        }
2775        return getSecondLegEndRouteLocation().getName();
2776    }
2777
2778    public RouteLocation getSecondLegEndRouteLocation() {
2779        return _end2Leg;
2780    }
2781
2782    public void setThirdLegEndRouteLocation(RouteLocation rl) {
2783        _leg3End = rl;
2784    }
2785
2786    public RouteLocation getThirdLegEndRouteLocation() {
2787        return _leg3End;
2788    }
2789
2790    public String getThirdLegEndLocationName() {
2791        if (getThirdLegEndRouteLocation() == null) {
2792            return NONE;
2793        }
2794        return getThirdLegEndRouteLocation().getName();
2795    }
2796
2797    /**
2798     * Optional changes to train while en route.
2799     *
2800     * @param options NO_CABOOSE_OR_FRED, CHANGE_ENGINES, ADD_CABOOSE,
2801     *                HELPER_ENGINES, REMOVE_CABOOSE
2802     */
2803    public void setSecondLegOptions(int options) {
2804        int old = _leg2Options;
2805        _leg2Options = options;
2806        if (old != options) {
2807            setDirtyAndFirePropertyChange("trainLegOptions", old, options); // NOI18N
2808        }
2809    }
2810
2811    public int getSecondLegOptions() {
2812        return _leg2Options;
2813    }
2814
2815    /**
2816     * Optional changes to train while en route.
2817     *
2818     * @param options NO_CABOOSE_OR_FRED, CHANGE_ENGINES, ADD_CABOOSE,
2819     *                HELPER_ENGINES, REMOVE_CABOOSE
2820     */
2821    public void setThirdLegOptions(int options) {
2822        int old = _leg3Options;
2823        _leg3Options = options;
2824        if (old != options) {
2825            setDirtyAndFirePropertyChange("trainLegOptions", old, options); // NOI18N
2826        }
2827    }
2828
2829    public int getThirdLegOptions() {
2830        return _leg3Options;
2831    }
2832
2833    public void setComment(String comment) {
2834        String old = _comment;
2835        _comment = comment;
2836        if (!old.equals(comment)) {
2837            setDirtyAndFirePropertyChange("trainComment", old, comment); // NOI18N
2838        }
2839    }
2840    
2841    public String getCommentCurrentWithColor() {
2842        return commentCurrent(getCommentWithColor());
2843    }
2844    
2845    public String getCommentCurrent() {
2846        return commentCurrent(getComment());
2847    }
2848
2849    public String getComment() {
2850        return TrainCommon.getOnlyText(getCommentWithColor());
2851    }
2852
2853    public String getCommentWithColor() {
2854        return _comment;
2855    }
2856    
2857    public Color getCommentColor() {
2858        return TrainCommon.getTextColor(getCommentWithColor());
2859    }
2860    
2861    public String getCommentColorName() {
2862        return TrainCommon.getTextColorName(getCommentWithColor());
2863    }
2864    
2865    private String commentCurrent(String comment) {
2866        RouteLocation crl = getCurrentRouteLocation();
2867        if (crl == null) {
2868            crl = getTrainDepartsRouteLocation();
2869        }
2870        return getCommentCurrent(comment, crl);
2871    }
2872    
2873    public String getCommentCurrent(String comment, RouteLocation crl) {
2874        return MessageFormat.format(comment,
2875                new Object[]{getSplitName(), getDescription(), getTrainDepartsName(),
2876                        getFormatedDepartureTime(), getTrainDepartsDirection(),
2877                        getTrainTerminatesName(), getNumberCarsInTrain(crl),
2878                        getNumberLoadedCarsInTrain(crl), getNumberEmptyCarsInTrain(crl),
2879                        getTrainLength(crl), Setup.getLengthUnit().toLowerCase(),
2880                        getTrainWeight(crl), getLeadEngineRoadAndNumber(),
2881                        getLeadEngineDccAddress()});
2882    }
2883
2884    /**
2885     * Add a script to run before a train is built
2886     *
2887     * @param pathname The script's pathname
2888     */
2889    public void addBuildScript(String pathname) {
2890        _buildScripts.add(pathname);
2891        setDirtyAndFirePropertyChange("addBuildScript", pathname, null); // NOI18N
2892    }
2893
2894    public void deleteBuildScript(String pathname) {
2895        _buildScripts.remove(pathname);
2896        setDirtyAndFirePropertyChange("deleteBuildScript", null, pathname); // NOI18N
2897    }
2898
2899    /**
2900     * Gets a list of pathnames (scripts) to run before this train is built
2901     *
2902     * @return A list of pathnames to run before this train is built
2903     */
2904    public List<String> getBuildScripts() {
2905        return _buildScripts;
2906    }
2907
2908    /**
2909     * Add a script to run after a train is built
2910     *
2911     * @param pathname The script's pathname
2912     */
2913    public void addAfterBuildScript(String pathname) {
2914        _afterBuildScripts.add(pathname);
2915        setDirtyAndFirePropertyChange("addAfterBuildScript", pathname, null); // NOI18N
2916    }
2917
2918    public void deleteAfterBuildScript(String pathname) {
2919        _afterBuildScripts.remove(pathname);
2920        setDirtyAndFirePropertyChange("deleteAfterBuildScript", null, pathname); // NOI18N
2921    }
2922
2923    /**
2924     * Gets a list of pathnames (scripts) to run after this train is built
2925     *
2926     * @return A list of pathnames to run after this train is built
2927     */
2928    public List<String> getAfterBuildScripts() {
2929        return _afterBuildScripts;
2930    }
2931
2932    /**
2933     * Add a script to run when train is moved
2934     *
2935     * @param pathname The script's pathname
2936     */
2937    public void addMoveScript(String pathname) {
2938        _moveScripts.add(pathname);
2939        setDirtyAndFirePropertyChange("addMoveScript", pathname, null); // NOI18N
2940    }
2941
2942    public void deleteMoveScript(String pathname) {
2943        _moveScripts.remove(pathname);
2944        setDirtyAndFirePropertyChange("deleteMoveScript", null, pathname); // NOI18N
2945    }
2946
2947    /**
2948     * Gets a list of pathnames (scripts) to run when this train moved
2949     *
2950     * @return A list of pathnames to run when this train moved
2951     */
2952    public List<String> getMoveScripts() {
2953        return _moveScripts;
2954    }
2955
2956    /**
2957     * Add a script to run when train is terminated
2958     *
2959     * @param pathname The script's pathname
2960     */
2961    public void addTerminationScript(String pathname) {
2962        _terminationScripts.add(pathname);
2963        setDirtyAndFirePropertyChange("addTerminationScript", pathname, null); // NOI18N
2964    }
2965
2966    public void deleteTerminationScript(String pathname) {
2967        _terminationScripts.remove(pathname);
2968        setDirtyAndFirePropertyChange("deleteTerminationScript", null, pathname); // NOI18N
2969    }
2970
2971    /**
2972     * Gets a list of pathnames (scripts) to run when this train terminates
2973     *
2974     * @return A list of pathnames to run when this train terminates
2975     */
2976    public List<String> getTerminationScripts() {
2977        return _terminationScripts;
2978    }
2979
2980    /**
2981     * Gets the optional railroad name for this train.
2982     *
2983     * @return Train's railroad name.
2984     */
2985    public String getRailroadName() {
2986        return _railroadName;
2987    }
2988
2989    /**
2990     * Overrides the default railroad name for this train.
2991     *
2992     * @param name The railroad name for this train.
2993     */
2994    public void setRailroadName(String name) {
2995        String old = _railroadName;
2996        _railroadName = name;
2997        if (!old.equals(name)) {
2998            setDirtyAndFirePropertyChange("trainRailroadName", old, name); // NOI18N
2999        }
3000    }
3001
3002    public String getManifestLogoPathName() {
3003        return _logoPathName;
3004    }
3005
3006    /**
3007     * Overrides the default logo for this train.
3008     *
3009     * @param pathName file location for the logo.
3010     */
3011    public void setManifestLogoPathName(String pathName) {
3012        _logoPathName = pathName;
3013    }
3014
3015    public boolean isShowArrivalAndDepartureTimesEnabled() {
3016        return _showTimes;
3017    }
3018
3019    public void setShowArrivalAndDepartureTimes(boolean enable) {
3020        boolean old = _showTimes;
3021        _showTimes = enable;
3022        if (old != enable) {
3023            setDirtyAndFirePropertyChange("showArrivalAndDepartureTimes", old, enable); // NOI18N
3024        }
3025    }
3026
3027    public boolean isSendCarsToTerminalEnabled() {
3028        return _sendToTerminal;
3029    }
3030
3031    public void setSendCarsToTerminalEnabled(boolean enable) {
3032        boolean old = _sendToTerminal;
3033        _sendToTerminal = enable;
3034        if (old != enable) {
3035            setDirtyAndFirePropertyChange("send cars to terminal", old, enable); // NOI18N
3036        }
3037    }
3038
3039    /**
3040     * Allow local moves if car has a custom load or Final Destination
3041     *
3042     * @return true if local move is allowed
3043     */
3044    public boolean isAllowLocalMovesEnabled() {
3045        return _allowLocalMoves;
3046    }
3047
3048    public void setAllowLocalMovesEnabled(boolean enable) {
3049        boolean old = _allowLocalMoves;
3050        _allowLocalMoves = enable;
3051        if (old != enable) {
3052            setDirtyAndFirePropertyChange("allow local moves", old, enable); // NOI18N
3053        }
3054    }
3055
3056    public boolean isAllowThroughCarsEnabled() {
3057        return _allowThroughCars;
3058    }
3059
3060    public void setAllowThroughCarsEnabled(boolean enable) {
3061        boolean old = _allowThroughCars;
3062        _allowThroughCars = enable;
3063        if (old != enable) {
3064            setDirtyAndFirePropertyChange("allow through cars", old, enable); // NOI18N
3065        }
3066    }
3067
3068    public boolean isBuildTrainNormalEnabled() {
3069        return _buildNormal;
3070    }
3071
3072    public void setBuildTrainNormalEnabled(boolean enable) {
3073        boolean old = _buildNormal;
3074        _buildNormal = enable;
3075        if (old != enable) {
3076            setDirtyAndFirePropertyChange("build train normal", old, enable); // NOI18N
3077        }
3078    }
3079
3080    /**
3081     * When true allow a turn to return cars to staging. A turn is a train that
3082     * departs and terminates at the same location.
3083     *
3084     * @return true if cars can return to staging
3085     */
3086    public boolean isAllowReturnToStagingEnabled() {
3087        return _allowCarsReturnStaging;
3088    }
3089
3090    public void setAllowReturnToStagingEnabled(boolean enable) {
3091        boolean old = _allowCarsReturnStaging;
3092        _allowCarsReturnStaging = enable;
3093        if (old != enable) {
3094            setDirtyAndFirePropertyChange("allow cars to return to staging", old, enable); // NOI18N
3095        }
3096    }
3097
3098    public boolean isServiceAllCarsWithFinalDestinationsEnabled() {
3099        return _serviceAllCarsWithFinalDestinations;
3100    }
3101
3102    public void setServiceAllCarsWithFinalDestinationsEnabled(boolean enable) {
3103        boolean old = _serviceAllCarsWithFinalDestinations;
3104        _serviceAllCarsWithFinalDestinations = enable;
3105        if (old != enable) {
3106            setDirtyAndFirePropertyChange("TrainServiceAllCarsWithFinalDestinations", old, enable); // NOI18N
3107        }
3108    }
3109
3110    public boolean isBuildConsistEnabled() {
3111        return _buildConsist;
3112    }
3113
3114    public void setBuildConsistEnabled(boolean enable) {
3115        boolean old = _buildConsist;
3116        _buildConsist = enable;
3117        if (old != enable) {
3118            setDirtyAndFirePropertyChange("TrainBuildConsist", old, enable); // NOI18N
3119        }
3120    }
3121
3122    public boolean isSendCarsWithCustomLoadsToStagingEnabled() {
3123        return _sendCarsWithCustomLoadsToStaging;
3124    }
3125
3126    public void setSendCarsWithCustomLoadsToStagingEnabled(boolean enable) {
3127        boolean old = _sendCarsWithCustomLoadsToStaging;
3128        _sendCarsWithCustomLoadsToStaging = enable;
3129        if (old != enable) {
3130            setDirtyAndFirePropertyChange("SendCarsWithCustomLoadsToStaging", old, enable); // NOI18N
3131        }
3132    }
3133    
3134    public boolean isBuilding() {
3135        return getStatusCode() == CODE_BUILDING;
3136    }
3137
3138    public void setBuilt(boolean built) {
3139        boolean old = _built;
3140        _built = built;
3141        if (old != built) {
3142            setDirtyAndFirePropertyChange(BUILT_CHANGED_PROPERTY, old, built); // NOI18N
3143        }
3144    }
3145
3146    /**
3147     * Used to determine if this train has been built.
3148     *
3149     * @return true if the train was successfully built.
3150     */
3151    public boolean isBuilt() {
3152        return _built;
3153    }
3154
3155    /**
3156     * Set true whenever the train's manifest has been modified. For example
3157     * adding or removing a car from a train, or changing the manifest format.
3158     * Once the manifest has been regenerated (modified == false), the old
3159     * status for the train is restored.
3160     *
3161     * @param modified True if train's manifest has been modified.
3162     */
3163    public void setModified(boolean modified) {
3164        log.debug("Set modified {}", modified);
3165        if (!isBuilt()) {
3166            _modified = false;
3167            return; // there isn't a manifest to modify
3168        }
3169        boolean old = _modified;
3170        _modified = modified;
3171        if (modified) {
3172            setPrinted(false);
3173        }
3174        if (old != modified) {
3175            if (modified) {
3176                // scripts can call setModified() for a train
3177                if (getStatusCode() != CODE_RUN_SCRIPTS) {
3178                    setOldStatusCode(getStatusCode());
3179                }
3180                setStatusCode(CODE_MANIFEST_MODIFIED);
3181            } else {
3182                setStatusCode(getOldStatusCode()); // restore previous train
3183                                                   // status
3184            }
3185        }
3186        setDirtyAndFirePropertyChange(TRAIN_MODIFIED_CHANGED_PROPERTY, null, modified); // NOI18N
3187    }
3188
3189    public boolean isModified() {
3190        return _modified;
3191    }
3192
3193    /**
3194     * Control flag used to decide if this train is to be built.
3195     *
3196     * @param build When true, build this train.
3197     */
3198    public void setBuildEnabled(boolean build) {
3199        boolean old = _build;
3200        _build = build;
3201        if (old != build) {
3202            setDirtyAndFirePropertyChange(BUILD_CHANGED_PROPERTY, old, build); // NOI18N
3203        }
3204    }
3205
3206    /**
3207     * Used to determine if train is to be built.
3208     *
3209     * @return true if train is to be built.
3210     */
3211    public boolean isBuildEnabled() {
3212        return _build;
3213    }
3214
3215    /**
3216     * Build this train if the build control flag is true.
3217     *
3218     * @return True only if train is successfully built.
3219     */
3220    public boolean buildIfSelected() {
3221        if (isBuildEnabled() && !isBuilt()) {
3222            return build();
3223        }
3224        log.debug("Train ({}) not selected or already built, skipping build", getName());
3225        return false;
3226    }
3227
3228    /**
3229     * Build this train. Creates a train manifest.
3230     *
3231     * @return True if build successful.
3232     */
3233    public synchronized boolean build() {
3234        TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
3235        if (!trainManager.checkBuildOrder(this)) {
3236            setStatusCode(CODE_ERROR);
3237            return false;
3238        }
3239        reset();
3240        // check to see if any other trains are building
3241        int count = 1200; // wait up to 120 seconds
3242        while (trainManager.isAnyTrainBuilding() && count > 0) {
3243            count--;
3244            try {
3245                wait(100); // 100 msec
3246            } catch (InterruptedException e) {
3247                // TODO Auto-generated catch block
3248                log.error("Thread unexpectedly interrupted", e);
3249            }
3250        }
3251        // timed out?
3252        if (count <= 0) {
3253            log.warn("Build timeout for train ({})", getName());
3254            setBuildFailed(true);
3255            setStatusCode(CODE_BUILD_FAILED);
3256            return false;
3257        }
3258        // run before build scripts
3259        runScripts(getBuildScripts());
3260        TrainBuilder tb = new TrainBuilder();
3261        boolean results = tb.build(this);
3262        // run after build scripts
3263        runScripts(getAfterBuildScripts());
3264        return results;
3265    }
3266
3267    /**
3268     * Run train scripts, waits for completion before returning.
3269     */
3270    private synchronized void runScripts(List<String> scripts) {
3271        if (scripts.size() > 0) {
3272            // save the current status
3273            setOldStatusCode(getStatusCode());
3274            setStatusCode(CODE_RUN_SCRIPTS);
3275            // create the python interpreter thread
3276            JmriScriptEngineManager.getDefault().initializeAllEngines();
3277            // find the number of active threads
3278            ThreadGroup root = Thread.currentThread().getThreadGroup();
3279            int numberOfThreads = root.activeCount();
3280            // log.debug("Number of active threads: {}", numberOfThreads);
3281            for (String scriptPathname : scripts) {
3282                try {
3283                    JmriScriptEngineManager.getDefault()
3284                            .runScript(new File(jmri.util.FileUtil.getExternalFilename(scriptPathname)));
3285                } catch (Exception e) {
3286                    log.error("Problem with script: {}", scriptPathname);
3287                }
3288            }
3289            // need to wait for scripts to complete or 4 seconds maximum
3290            int count = 0;
3291            while (root.activeCount() > numberOfThreads) {
3292                log.debug("Number of active threads: {}, at start: {}", root.activeCount(), numberOfThreads);
3293                try {
3294                    wait(40);
3295                } catch (InterruptedException e) {
3296                    Thread.currentThread().interrupt();
3297                }
3298                if (count++ > 100) {
3299                    break; // 4 seconds maximum 40*100 = 4000
3300                }
3301            }
3302            setStatusCode(getOldStatusCode());
3303        }
3304    }
3305
3306    public boolean printBuildReport() {
3307        boolean isPreview = (InstanceManager.getDefault(TrainManager.class).isPrintPreviewEnabled() ||
3308                Setup.isBuildReportAlwaysPreviewEnabled());
3309        return printBuildReport(isPreview);
3310    }
3311
3312    public boolean printBuildReport(boolean isPreview) {
3313        File buildFile = InstanceManager.getDefault(TrainManagerXml.class).getTrainBuildReportFile(getName());
3314        if (!buildFile.exists()) {
3315            log.warn("Build file missing for train {}", getName());
3316            return false;
3317        }
3318
3319        if (isPreview && Setup.isBuildReportEditorEnabled()) {
3320            TrainPrintBuildReport.editReport(buildFile, getName());
3321        } else {
3322            TrainPrintBuildReport.printReport(buildFile,
3323                    Bundle.getMessage("buildReport", getDescription()), isPreview);
3324        }
3325        return true;
3326    }
3327
3328    public void setBuildFailed(boolean status) {
3329        boolean old = _buildFailed;
3330        _buildFailed = status;
3331        if (old != status) {
3332            setDirtyAndFirePropertyChange("buildFailed", old, status); // NOI18N
3333        }
3334    }
3335
3336    /**
3337     * Returns true if the train build failed. Note that returning false doesn't
3338     * mean the build was successful.
3339     *
3340     * @return true if train build failed.
3341     */
3342    public boolean isBuildFailed() {
3343        return _buildFailed;
3344    }
3345
3346    public void setBuildFailedMessage(String message) {
3347        String old = _buildFailedMessage;
3348        _buildFailedMessage = message;
3349        if (!old.equals(message)) {
3350            setDirtyAndFirePropertyChange("buildFailedMessage", old, message); // NOI18N
3351        }
3352    }
3353
3354    protected String getBuildFailedMessage() {
3355        return _buildFailedMessage;
3356    }
3357
3358    /**
3359     * Print manifest for train if already built.
3360     *
3361     * @return true if print successful.
3362     */
3363    public boolean printManifestIfBuilt() {
3364        if (isBuilt()) {
3365            boolean isPreview = InstanceManager.getDefault(TrainManager.class).isPrintPreviewEnabled();
3366            try {
3367                return (printManifest(isPreview));
3368            } catch (BuildFailedException e) {
3369                log.error("Print Manifest failed: {}", e.getMessage());
3370            }
3371        } else {
3372            log.debug("Need to build train ({}) before printing manifest", getName());
3373        }
3374        return false;
3375    }
3376
3377    /**
3378     * Print manifest for train.
3379     *
3380     * @param isPreview True if preview.
3381     * @return true if print successful, false if train print file not found.
3382     * @throws BuildFailedException if unable to create new Manifests
3383     */
3384    public boolean printManifest(boolean isPreview) throws BuildFailedException {
3385        if (isModified()) {
3386            new TrainManifest(this);
3387            try {
3388                new JsonManifest(this).build();
3389            } catch (IOException ex) {
3390                log.error("Unable to create JSON manifest {}", ex.getLocalizedMessage());
3391            }
3392            new TrainCsvManifest(this);
3393        }
3394        File file = InstanceManager.getDefault(TrainManagerXml.class).getTrainManifestFile(getName());
3395        if (!file.exists()) {
3396            log.warn("Manifest file missing for train ({})", getName());
3397            return false;
3398        }
3399        if (isPreview && Setup.isManifestEditorEnabled()) {
3400            TrainUtilities.openDesktop(file);
3401            return true;
3402        }
3403        String logoURL = Setup.NONE;
3404        if (!getManifestLogoPathName().equals(NONE)) {
3405            logoURL = FileUtil.getExternalFilename(getManifestLogoPathName());
3406        } else if (!Setup.getManifestLogoURL().equals(Setup.NONE)) {
3407            logoURL = FileUtil.getExternalFilename(Setup.getManifestLogoURL());
3408        }
3409        Location departs = InstanceManager.getDefault(LocationManager.class).getLocationByName(getTrainDepartsName());
3410        String printerName = Location.NONE;
3411        if (departs != null) {
3412            printerName = departs.getDefaultPrinterName();
3413        }
3414        // the train description shouldn't exceed half of the page width or the
3415        // page number will be overwritten
3416        String name = getDescription();
3417        if (name.length() > TrainCommon.getManifestHeaderLineLength() / 2) {
3418            name = name.substring(0, TrainCommon.getManifestHeaderLineLength() / 2);
3419        }
3420        TrainPrintManifest.printReport(file, name, isPreview, Setup.getFontName(), logoURL, printerName,
3421                Setup.getManifestOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(),
3422                Setup.getPrintDuplexSides());
3423        if (!isPreview) {
3424            setPrinted(true);
3425        }
3426        return true;
3427    }
3428
3429    public boolean openFile() {
3430        File file = createCsvManifestFile();
3431        if (file == null || !file.exists()) {
3432            log.warn("CSV manifest file missing for train {}", getName());
3433            return false;
3434        }
3435        TrainUtilities.openDesktop(file);
3436        return true;
3437    }
3438
3439    public boolean runFile() {
3440        File file = createCsvManifestFile();
3441        if (file == null || !file.exists()) {
3442            log.warn("CSV manifest file missing for train {}", getName());
3443            return false;
3444        }
3445        // Set up to process the CSV file by the external Manifest program
3446        InstanceManager.getDefault(TrainCustomManifest.class).addCsvFile(file);
3447        if (!InstanceManager.getDefault(TrainCustomManifest.class).process()) {
3448            if (!InstanceManager.getDefault(TrainCustomManifest.class).doesExcelFileExist()) {
3449                JmriJOptionPane.showMessageDialog(null,
3450                        Bundle.getMessage("LoadDirectoryNameFileName",
3451                                InstanceManager.getDefault(TrainCustomManifest.class).getDirectoryPathName(),
3452                                InstanceManager.getDefault(TrainCustomManifest.class).getFileName()),
3453                        Bundle.getMessage("ManifestCreatorNotFound"), JmriJOptionPane.ERROR_MESSAGE);
3454            }
3455            return false;
3456        }
3457        return true;
3458    }
3459
3460    public File createCsvManifestFile() {
3461        if (isModified()) {
3462            try {
3463                new TrainManifest(this);
3464                try {
3465                    new JsonManifest(this).build();
3466                } catch (IOException ex) {
3467                    log.error("Unable to create JSON manifest {}", ex.getLocalizedMessage());
3468                }
3469                new TrainCsvManifest(this);
3470            } catch (BuildFailedException e) {
3471                log.error("Could not create CVS Manifest files");
3472            }
3473        }
3474        File file = InstanceManager.getDefault(TrainManagerXml.class).getTrainCsvManifestFile(getName());
3475        if (!file.exists()) {
3476            log.warn("CSV manifest file was not created for train ({})", getName());
3477            return null;
3478        }
3479        return file;
3480    }
3481
3482    public void setPrinted(boolean printed) {
3483        boolean old = _printed;
3484        _printed = printed;
3485        if (old != printed) {
3486            setDirtyAndFirePropertyChange("trainPrinted", old, printed); // NOI18N
3487        }
3488    }
3489
3490    /**
3491     * Used to determine if train manifest was printed.
3492     *
3493     * @return true if the train manifest was printed.
3494     */
3495    public boolean isPrinted() {
3496        return _printed;
3497    }
3498
3499    /**
3500     * Sets the panel position for the train icon for the current route
3501     * location.
3502     *
3503     * @return true if train coordinates can be set
3504     */
3505    public boolean setTrainIconCoordinates() {
3506        if (Setup.isTrainIconCordEnabled() && getCurrentRouteLocation() != null && _trainIcon != null) {
3507            getCurrentRouteLocation().setTrainIconX(_trainIcon.getX());
3508            getCurrentRouteLocation().setTrainIconY(_trainIcon.getY());
3509            return true;
3510        }
3511        return false;
3512    }
3513
3514    /**
3515     * Terminate train.
3516     */
3517    public void terminate() {
3518        while (isBuilt()) {
3519            move();
3520        }
3521    }
3522
3523    /**
3524     * Move train to next location in the route. Will move engines, cars, and
3525     * train icon. Will also terminate a train after it arrives at its final
3526     * destination.
3527     */
3528    public void move() {
3529        log.debug("Move train ({})", getName());
3530        if (getRoute() == null || getCurrentRouteLocation() == null) {
3531            setBuilt(false); // break terminate loop
3532            return;
3533        }
3534        if (!isBuilt()) {
3535            log.error("ERROR attempt to move train ({}) that hasn't been built", getName());
3536            return;
3537        }
3538        RouteLocation rl = getCurrentRouteLocation();
3539        RouteLocation rlNext = getNextRouteLocation(rl);
3540
3541        setCurrentLocation(rlNext);
3542
3543        // cars and engines will move via property change
3544        setDirtyAndFirePropertyChange(TRAIN_LOCATION_CHANGED_PROPERTY, rl, rlNext);
3545        moveTrainIcon(rlNext);
3546        updateStatus(rl, rlNext);
3547        // tell GUI that train has complete its move
3548        setDirtyAndFirePropertyChange(TRAIN_MOVE_COMPLETE_CHANGED_PROPERTY, rl, rlNext);
3549    }
3550
3551    /**
3552     * Move train to a location in the train's route. Code checks to see if the
3553     * location requested is part of the train's route and if the train hasn't
3554     * already visited the location. This command can only move the train
3555     * forward in its route. Note that you can not terminate the train using
3556     * this command. See move() or terminate().
3557     *
3558     * @param locationName The name of the location to move this train.
3559     * @return true if train was able to move to the named location.
3560     */
3561    public boolean move(String locationName) {
3562        log.info("Move train ({}) to location ({})", getName(), locationName);
3563        if (getRoute() == null || getCurrentRouteLocation() == null) {
3564            return false;
3565        }
3566        List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
3567        for (int i = 0; i < routeList.size(); i++) {
3568            RouteLocation rl = routeList.get(i);
3569            if (getCurrentRouteLocation() == rl) {
3570                for (int j = i + 1; j < routeList.size(); j++) {
3571                    rl = routeList.get(j);
3572                    if (rl.getName().equals(locationName)) {
3573                        log.debug("Found location ({}) moving train to this location", locationName);
3574                        for (j = i + 1; j < routeList.size(); j++) {
3575                            rl = routeList.get(j);
3576                            move();
3577                            if (rl.getName().equals(locationName)) {
3578                                return true;
3579                            }
3580                        }
3581                    }
3582                }
3583                break; // done
3584            }
3585        }
3586        return false;
3587    }
3588
3589    /**
3590     * Moves the train to the specified route location
3591     *
3592     * @param rl route location
3593     * @return true if successful
3594     */
3595    public boolean move(RouteLocation rl) {
3596        if (rl == null) {
3597            return false;
3598        }
3599        log.debug("Move train ({}) to location ({})", getName(), rl.getName());
3600        if (getRoute() == null || getCurrentRouteLocation() == null) {
3601            return false;
3602        }
3603        boolean foundCurrent = false;
3604        for (RouteLocation xrl : getRoute().getLocationsBySequenceList()) {
3605            if (getCurrentRouteLocation() == xrl) {
3606                foundCurrent = true;
3607            }
3608            if (xrl == rl) {
3609                if (foundCurrent) {
3610                    return true; // done
3611                } else {
3612                    break; // train passed this location
3613                }
3614            }
3615            if (foundCurrent) {
3616                move();
3617            }
3618        }
3619        return false;
3620    }
3621
3622    /**
3623     * Move train to the next location in the train's route. The location name
3624     * provided must be equal to the next location name in the train's route.
3625     *
3626     * @param locationName The next location name in the train's route.
3627     * @return true if successful.
3628     */
3629    public boolean moveToNextLocation(String locationName) {
3630        if (getNextLocationName().equals(locationName)) {
3631            move();
3632            return true;
3633        }
3634        return false;
3635    }
3636
3637    public void loadTrainIcon() {
3638        if (getCurrentRouteLocation() != null) {
3639            moveTrainIcon(getCurrentRouteLocation());
3640        }
3641    }
3642
3643    private final boolean animation = true; // when true use animation for icon
3644                                            // moves
3645    TrainIconAnimation _ta;
3646
3647    /*
3648     * The train icon is moved to route location (rl) for this train
3649     */
3650    public void moveTrainIcon(RouteLocation rl) {
3651        // create train icon if at departure, if program has been restarted, or removed
3652        if (rl == getTrainDepartsRouteLocation() || _trainIcon == null || !_trainIcon.isActive()) {
3653            createTrainIcon(rl);
3654        }
3655        // is the lead engine still in train
3656        if (getLeadEngine() != null && getLeadEngine().getRouteDestination() == rl && rl != null) {
3657            log.debug("Engine ({}) arriving at destination {}", getLeadEngine().toString(), rl.getName());
3658        }
3659        if (_trainIcon != null && _trainIcon.isActive()) {
3660            setTrainIconColor();
3661            _trainIcon.setShowToolTip(true);
3662            String txt = null;
3663            if (getCurrentLocationName().equals(NONE)) {
3664                txt = getDescription() + " " + Bundle.getMessage("Terminated") + " (" + getTrainTerminatesName() + ")";
3665            } else {
3666                txt = Bundle.getMessage("TrainAtNext",
3667                        getDescription(), getCurrentLocationName(), getNextLocationName(), getTrainLength(),
3668                        Setup.getLengthUnit().toLowerCase());
3669            }
3670            _trainIcon.getToolTip().setText(txt);
3671            _trainIcon.getToolTip().setBackgroundColor(Color.white);
3672            // rl can be null when train is terminated.
3673            if (rl != null) {
3674                if (rl.getTrainIconX() != 0 || rl.getTrainIconY() != 0) {
3675                    if (animation) {
3676                        TrainIconAnimation ta = new TrainIconAnimation(_trainIcon, rl, _ta);
3677                        ta.start(); // start the animation
3678                        _ta = ta;
3679                    } else {
3680                        _trainIcon.setLocation(rl.getTrainIconX(), rl.getTrainIconY());
3681                    }
3682                }
3683            }
3684        }
3685    }
3686
3687    public String getIconName() {
3688        String name = getSplitName();
3689        if (isBuilt() && getLeadEngine() != null && Setup.isTrainIconAppendEnabled()) {
3690            name += " " + getLeadEngineNumber();
3691        }
3692        return name;
3693    }
3694
3695    public String getLeadEngineNumber() {
3696        if (getLeadEngine() == null) {
3697            return NONE;
3698        }
3699        if (getLeadEngine().isClone()) {
3700            return getLeadEngine().getNumber().split(Engine.CLONE_REGEX)[0];
3701        }
3702        return getLeadEngine().getNumber();
3703    }
3704
3705    public String getLeadEngineRoadName() {
3706        if (getLeadEngine() == null) {
3707            return NONE;
3708        }
3709        return getLeadEngine().getRoadName();
3710    }
3711
3712    public String getLeadEngineRoadAndNumber() {
3713        if (getLeadEngine() == null) {
3714            return NONE;
3715        }
3716        return getLeadEngineRoadName() + " " + getLeadEngineNumber();
3717    }
3718
3719    public String getLeadEngineDccAddress() {
3720        if (getLeadEngine() == null) {
3721            return NONE;
3722        }
3723        return getLeadEngine().getDccAddress();
3724    }
3725
3726    /**
3727     * Gets the lead engine, will create it if the program has been restarted
3728     *
3729     * @return lead engine for this train
3730     */
3731    public Engine getLeadEngine() {
3732        if (_leadEngine == null && !_leadEngineId.equals(NONE)) {
3733            _leadEngine = InstanceManager.getDefault(EngineManager.class).getById(_leadEngineId);
3734        }
3735        return _leadEngine;
3736    }
3737
3738    public void setLeadEngine(Engine engine) {
3739        if (engine == null) {
3740            _leadEngineId = NONE;
3741        }
3742        _leadEngine = engine;
3743    }
3744
3745    /**
3746     * Returns the lead engine in a train's route. There can be up to two
3747     * changes in the lead engine for a train.
3748     *
3749     * @param routeLocation where in the train's route to find the lead engine.
3750     * @return lead engine
3751     */
3752    public Engine getLeadEngine(RouteLocation routeLocation) {
3753        Engine lead = null;
3754        for (RouteLocation rl : getRoute().getLocationsBySequenceList()) {
3755            for (Engine engine : InstanceManager.getDefault(EngineManager.class).getByTrainList(this)) {
3756                if (engine.getRouteLocation() == rl && (engine.getConsist() == null || engine.isLead())) {
3757                    lead = engine;
3758                    break;
3759                }
3760            }
3761            if (rl == routeLocation) {
3762                break;
3763            }
3764        }
3765        return lead;
3766    }
3767
3768    protected TrainIcon _trainIcon = null;
3769
3770    public TrainIcon getTrainIcon() {
3771        return _trainIcon;
3772    }
3773
3774    public void createTrainIcon(RouteLocation rl) {
3775        if (_trainIcon != null && _trainIcon.isActive()) {
3776            _trainIcon.remove();
3777        }
3778        // if there's a panel specified, get it and place icon
3779        if (!Setup.getPanelName().isEmpty()) {
3780            Editor editor = InstanceManager.getDefault(EditorManager.class).getTargetFrame(Setup.getPanelName());
3781            if (editor != null) {
3782                try {
3783                    _trainIcon = editor.addTrainIcon(getIconName());
3784                } catch (Exception e) {
3785                    log.error("Error placing train ({}) icon on panel ({})", getName(), Setup.getPanelName(), e);
3786                    return;
3787                }
3788                _trainIcon.setTrain(this);
3789                if (getIconName().length() > 9) {
3790                    _trainIcon.setFont(_trainIcon.getFont().deriveFont(8.f));
3791                }
3792                if (rl != null) {
3793                    _trainIcon.setLocation(rl.getTrainIconX(), rl.getTrainIconY());
3794                }
3795                // add throttle if there's a throttle manager
3796                if (jmri.InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
3797                    // add throttle if JMRI loco roster entry exist
3798                    RosterEntry entry = null;
3799                    if (getLeadEngine() != null) {
3800                        // first try and find a match based on loco road number
3801                        entry = getLeadEngine().getRosterEntry();
3802                    }
3803                    if (entry != null) {
3804                        _trainIcon.setRosterEntry(entry);
3805                        if (getLeadEngine().getConsist() != null) {
3806                            _trainIcon.setConsistNumber(getLeadEngine().getConsist().getConsistNumber());
3807                        }
3808                    } else {
3809                        log.debug("Loco roster entry not found for train ({})", getName());
3810                    }
3811                }
3812            }
3813        }
3814    }
3815
3816    private void setTrainIconColor() {
3817        // Terminated train?
3818        if (getCurrentLocationName().equals(NONE)) {
3819            _trainIcon.setLocoColor(Setup.getTrainIconColorTerminate());
3820            return;
3821        }
3822        // local train serving only one location?
3823        if (isLocalSwitcher()) {
3824            _trainIcon.setLocoColor(Setup.getTrainIconColorLocal());
3825            return;
3826        }
3827        // set color based on train direction at current location
3828        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.NORTH) {
3829            _trainIcon.setLocoColor(Setup.getTrainIconColorNorth());
3830        }
3831        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.SOUTH) {
3832            _trainIcon.setLocoColor(Setup.getTrainIconColorSouth());
3833        }
3834        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.EAST) {
3835            _trainIcon.setLocoColor(Setup.getTrainIconColorEast());
3836        }
3837        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.WEST) {
3838            _trainIcon.setLocoColor(Setup.getTrainIconColorWest());
3839        }
3840    }
3841
3842    private void updateStatus(RouteLocation old, RouteLocation next) {
3843        if (next != null) {
3844            setStatusCode(CODE_TRAIN_EN_ROUTE);
3845            // run move scripts
3846            runScripts(getMoveScripts());
3847        } else {
3848            log.debug("Train ({}) terminated", getName());
3849            setStatusCode(CODE_TERMINATED);
3850            setBuilt(false);
3851            // run termination scripts
3852            runScripts(getTerminationScripts());
3853        }
3854    }
3855
3856    /**
3857     * Sets the print status for switch lists
3858     *
3859     * @param status UNKNOWN PRINTED
3860     */
3861    public void setSwitchListStatus(String status) {
3862        String old = _switchListStatus;
3863        _switchListStatus = status;
3864        if (!old.equals(status)) {
3865            setDirtyAndFirePropertyChange("switch list train status", old, status); // NOI18N
3866        }
3867    }
3868
3869    public String getSwitchListStatus() {
3870        return _switchListStatus;
3871    }
3872
3873    /**
3874     * Resets the train, removes engines and cars from this train.
3875     *
3876     * @return true if reset successful
3877     */
3878    public boolean reset() {
3879        // is this train in route?
3880        if (isTrainEnRoute()) {
3881            log.info("Train ({}) has started its route, can not be reset", getName());
3882            return false;
3883        }
3884        setCurrentLocation(null);
3885        setDepartureTrack(null);
3886        setTerminationTrack(null);
3887        setBuilt(false);
3888        setBuildFailed(false);
3889        setBuildFailedMessage(NONE);
3890        setPrinted(false);
3891        setModified(false);
3892        // remove cars and engines from this train via property change
3893        setStatusCode(CODE_TRAIN_RESET);
3894        // remove train icon
3895        if (_trainIcon != null && _trainIcon.isActive()) {
3896            _trainIcon.remove();
3897        }
3898        return true;
3899    }
3900    
3901    /**
3902     * Checks to see if the train's staging departure track has been taken by another train.
3903     * @return True if track has been allocated to another train.
3904     */
3905    public boolean checkDepartureTrack() {
3906        if (Setup.isStagingTrackImmediatelyAvail() &&
3907                !isTrainEnRoute() &&
3908                getDepartureTrack() != null &&
3909                getDepartureTrack().isStaging() &&
3910                getDepartureTrack() != getTerminationTrack() &&
3911                getDepartureTrack().getIgnoreUsedLengthPercentage() == Track.IGNORE_0) {
3912            if (getDepartureTrack().isQuickServiceEnabled()) {
3913                return getDepartureTrack().getNumberRS() > 0;
3914            }
3915            return getDepartureTrack().getDropRS() > 0;
3916        }
3917        return false;
3918    }
3919    
3920    /**
3921     * Used to determine if rolling stock was pulled before the current build
3922     * route location. If rolling stock before pulled current route location,
3923     * track space is available.
3924     * @param rs The rolling stock to be placed
3925     * 
3926     * @param r rolling stock to be tested for timing
3927     * @return true if rolling stock was pulled
3928     */
3929    public boolean checkPullTiming(RollingStock rs, RollingStock r) {
3930        // go thought the train's route to determine if rolling stock was already pulled.
3931        for (RouteLocation rl : getRoute().getLocationsBySequenceList()) {
3932            if (rl == r.getRouteLocation()) {
3933                break;
3934            }
3935            if (rl == rs.getRouteDestinationTiming()) {
3936                return false;
3937            }
3938        }
3939        return true;
3940    }
3941
3942    public void dispose() {
3943        if (getRoute() != null) {
3944            getRoute().removePropertyChangeListener(this);
3945        }
3946        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
3947        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
3948        InstanceManager.getDefault(EngineTypes.class).removePropertyChangeListener(this);
3949        InstanceManager.getDefault(CarOwners.class).removePropertyChangeListener(this);
3950        InstanceManager.getDefault(EngineModels.class).removePropertyChangeListener(this);
3951
3952        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, "Dispose"); // NOI18N
3953    }
3954
3955    /**
3956     * Construct this Entry from XML. This member has to remain synchronized
3957     * with the detailed DTD in operations-trains.dtd
3958     *
3959     * @param e Consist XML element
3960     */
3961    public Train(Element e) {
3962        org.jdom2.Attribute a;
3963        if ((a = e.getAttribute(Xml.ID)) != null) {
3964            _id = a.getValue();
3965        } else {
3966            log.warn("no id attribute in train element when reading operations");
3967        }
3968        if ((a = e.getAttribute(Xml.NAME)) != null) {
3969            _name = a.getValue();
3970        }
3971        if ((a = e.getAttribute(Xml.DESCRIPTION)) != null) {
3972            _description = a.getValue();
3973        }
3974        if ((a = e.getAttribute(Xml.DEPART_HOUR)) != null) {
3975            String day = "0";
3976            String hour = a.getValue();
3977            if ((a = e.getAttribute(Xml.DEPART_MINUTE)) != null) {
3978                String minute = a.getValue();
3979                if ((a = e.getAttribute(Xml.DEPART_DAY)) != null) {
3980                    day = a.getValue();
3981                }
3982                _departureTime = day + ":" + hour + ":" + minute;
3983            }
3984        }
3985
3986        // Trains table row color
3987        Element eRowColor = e.getChild(Xml.ROW_COLOR);
3988        if (eRowColor != null && (a = eRowColor.getAttribute(Xml.NAME)) != null) {
3989            _tableRowColorName = a.getValue().toLowerCase();
3990        }
3991        if (eRowColor != null && (a = eRowColor.getAttribute(Xml.RESET_ROW_COLOR)) != null) {
3992            _tableRowColorResetName = a.getValue().toLowerCase();
3993        }
3994
3995        Element eRoute = e.getChild(Xml.ROUTE);
3996        if (eRoute != null) {
3997            if ((a = eRoute.getAttribute(Xml.ID)) != null) {
3998                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteById(a.getValue()));
3999            }
4000            if (eRoute.getChild(Xml.SKIPS) != null) {
4001                List<Element> skips = eRoute.getChild(Xml.SKIPS).getChildren(Xml.LOCATION);
4002                String[] locs = new String[skips.size()];
4003                for (int i = 0; i < skips.size(); i++) {
4004                    Element loc = skips.get(i);
4005                    if ((a = loc.getAttribute(Xml.ID)) != null) {
4006                        locs[i] = a.getValue();
4007                    }
4008                }
4009                setTrainSkipsLocations(locs);
4010            }
4011        } else {
4012            // old format
4013            // try and first get the route by id then by name
4014            if ((a = e.getAttribute(Xml.ROUTE_ID)) != null) {
4015                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteById(a.getValue()));
4016            } else if ((a = e.getAttribute(Xml.ROUTE)) != null) {
4017                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteByName(a.getValue()));
4018            }
4019            if ((a = e.getAttribute(Xml.SKIP)) != null) {
4020                String locationIds = a.getValue();
4021                String[] locs = locationIds.split("%%"); // NOI18N
4022                // log.debug("Train skips: {}", locationIds);
4023                setTrainSkipsLocations(locs);
4024            }
4025        }
4026        // new way of reading car types using elements
4027        if (e.getChild(Xml.TYPES) != null) {
4028            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
4029            String[] types = new String[carTypes.size()];
4030            for (int i = 0; i < carTypes.size(); i++) {
4031                Element type = carTypes.get(i);
4032                if ((a = type.getAttribute(Xml.NAME)) != null) {
4033                    types[i] = a.getValue();
4034                }
4035            }
4036            setTypeNames(types);
4037            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
4038            types = new String[locoTypes.size()];
4039            for (int i = 0; i < locoTypes.size(); i++) {
4040                Element type = locoTypes.get(i);
4041                if ((a = type.getAttribute(Xml.NAME)) != null) {
4042                    types[i] = a.getValue();
4043                }
4044            }
4045            setTypeNames(types);
4046        } // old way of reading car types up to version 2.99.6
4047        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
4048            String names = a.getValue();
4049            String[] types = names.split("%%"); // NOI18N
4050            // log.debug("Car types: {}", names);
4051            setTypeNames(types);
4052        }
4053        // old misspelled format
4054        if ((a = e.getAttribute(Xml.CAR_ROAD_OPERATION)) != null) {
4055            _carRoadOption = a.getValue();
4056        }
4057        if ((a = e.getAttribute(Xml.CAR_ROAD_OPTION)) != null) {
4058            _carRoadOption = a.getValue();
4059        }
4060        // new way of reading car roads using elements
4061        if (e.getChild(Xml.CAR_ROADS) != null) {
4062            List<Element> carRoads = e.getChild(Xml.CAR_ROADS).getChildren(Xml.CAR_ROAD);
4063            String[] roads = new String[carRoads.size()];
4064            for (int i = 0; i < carRoads.size(); i++) {
4065                Element road = carRoads.get(i);
4066                if ((a = road.getAttribute(Xml.NAME)) != null) {
4067                    roads[i] = a.getValue();
4068                }
4069            }
4070            setCarRoadNames(roads);
4071        } // old way of reading car roads up to version 2.99.6
4072        else if ((a = e.getAttribute(Xml.CAR_ROADS)) != null) {
4073            String names = a.getValue();
4074            String[] roads = names.split("%%"); // NOI18N
4075            log.debug("Train ({}) {} car roads: {}", getName(), getCarRoadOption(), names);
4076            setCarRoadNames(roads);
4077        }
4078
4079        if ((a = e.getAttribute(Xml.CABOOSE_ROAD_OPTION)) != null) {
4080            _cabooseRoadOption = a.getValue();
4081        }
4082        // new way of reading caboose roads using elements
4083        if (e.getChild(Xml.CABOOSE_ROADS) != null) {
4084            List<Element> carRoads = e.getChild(Xml.CABOOSE_ROADS).getChildren(Xml.CAR_ROAD);
4085            String[] roads = new String[carRoads.size()];
4086            for (int i = 0; i < carRoads.size(); i++) {
4087                Element road = carRoads.get(i);
4088                if ((a = road.getAttribute(Xml.NAME)) != null) {
4089                    roads[i] = a.getValue();
4090                }
4091            }
4092            setCabooseRoadNames(roads);
4093        }
4094
4095        if ((a = e.getAttribute(Xml.LOCO_ROAD_OPTION)) != null) {
4096            _locoRoadOption = a.getValue();
4097        }
4098        // new way of reading engine roads using elements
4099        if (e.getChild(Xml.LOCO_ROADS) != null) {
4100            List<Element> locoRoads = e.getChild(Xml.LOCO_ROADS).getChildren(Xml.LOCO_ROAD);
4101            String[] roads = new String[locoRoads.size()];
4102            for (int i = 0; i < locoRoads.size(); i++) {
4103                Element road = locoRoads.get(i);
4104                if ((a = road.getAttribute(Xml.NAME)) != null) {
4105                    roads[i] = a.getValue();
4106                }
4107            }
4108            setLocoRoadNames(roads);
4109        }
4110
4111        if ((a = e.getAttribute(Xml.CAR_LOAD_OPTION)) != null) {
4112            _loadOption = a.getValue();
4113        }
4114        if ((a = e.getAttribute(Xml.CAR_OWNER_OPTION)) != null) {
4115            _ownerOption = a.getValue();
4116        }
4117        if ((a = e.getAttribute(Xml.BUILT_START_YEAR)) != null) {
4118            _builtStartYear = a.getValue();
4119        }
4120        if ((a = e.getAttribute(Xml.BUILT_END_YEAR)) != null) {
4121            _builtEndYear = a.getValue();
4122        }
4123        // new way of reading car loads using elements
4124        if (e.getChild(Xml.CAR_LOADS) != null) {
4125            List<Element> carLoads = e.getChild(Xml.CAR_LOADS).getChildren(Xml.CAR_LOAD);
4126            String[] loads = new String[carLoads.size()];
4127            for (int i = 0; i < carLoads.size(); i++) {
4128                Element load = carLoads.get(i);
4129                if ((a = load.getAttribute(Xml.NAME)) != null) {
4130                    loads[i] = a.getValue();
4131                }
4132            }
4133            setLoadNames(loads);
4134        } // old way of reading car loads up to version 2.99.6
4135        else if ((a = e.getAttribute(Xml.CAR_LOADS)) != null) {
4136            String names = a.getValue();
4137            String[] loads = names.split("%%"); // NOI18N
4138            log.debug("Train ({}) {} car loads: {}", getName(), getLoadOption(), names);
4139            setLoadNames(loads);
4140        }
4141        // new way of reading car owners using elements
4142        if (e.getChild(Xml.CAR_OWNERS) != null) {
4143            List<Element> carOwners = e.getChild(Xml.CAR_OWNERS).getChildren(Xml.CAR_OWNER);
4144            String[] owners = new String[carOwners.size()];
4145            for (int i = 0; i < carOwners.size(); i++) {
4146                Element owner = carOwners.get(i);
4147                if ((a = owner.getAttribute(Xml.NAME)) != null) {
4148                    owners[i] = a.getValue();
4149                }
4150            }
4151            setOwnerNames(owners);
4152        } // old way of reading car owners up to version 2.99.6
4153        else if ((a = e.getAttribute(Xml.CAR_OWNERS)) != null) {
4154            String names = a.getValue();
4155            String[] owners = names.split("%%"); // NOI18N
4156            log.debug("Train ({}) {} car owners: {}", getName(), getOwnerOption(), names);
4157            setOwnerNames(owners);
4158        }
4159
4160        if ((a = e.getAttribute(Xml.NUMBER_ENGINES)) != null) {
4161            _numberEngines = a.getValue();
4162        }
4163        if ((a = e.getAttribute(Xml.LEG2_ENGINES)) != null) {
4164            _leg2Engines = a.getValue();
4165        }
4166        if ((a = e.getAttribute(Xml.LEG3_ENGINES)) != null) {
4167            _leg3Engines = a.getValue();
4168        }
4169        if ((a = e.getAttribute(Xml.ENGINE_ROAD)) != null) {
4170            _engineRoad = a.getValue();
4171        }
4172        if ((a = e.getAttribute(Xml.LEG2_ROAD)) != null) {
4173            _leg2Road = a.getValue();
4174        }
4175        if ((a = e.getAttribute(Xml.LEG3_ROAD)) != null) {
4176            _leg3Road = a.getValue();
4177        }
4178        if ((a = e.getAttribute(Xml.ENGINE_MODEL)) != null) {
4179            _engineModel = a.getValue();
4180        }
4181        if ((a = e.getAttribute(Xml.LEG2_MODEL)) != null) {
4182            _leg2Model = a.getValue();
4183        }
4184        if ((a = e.getAttribute(Xml.LEG3_MODEL)) != null) {
4185            _leg3Model = a.getValue();
4186        }
4187        if ((a = e.getAttribute(Xml.REQUIRES)) != null) {
4188            try {
4189                _requires = Integer.parseInt(a.getValue());
4190            } catch (NumberFormatException ee) {
4191                log.error("Requires ({}) isn't a valid number for train ({})", a.getValue(), getName());
4192            }
4193        }
4194        if ((a = e.getAttribute(Xml.CABOOSE_ROAD)) != null) {
4195            _cabooseRoad = a.getValue();
4196        }
4197        if ((a = e.getAttribute(Xml.LEG2_CABOOSE_ROAD)) != null) {
4198            _leg2CabooseRoad = a.getValue();
4199        }
4200        if ((a = e.getAttribute(Xml.LEG3_CABOOSE_ROAD)) != null) {
4201            _leg3CabooseRoad = a.getValue();
4202        }
4203        if ((a = e.getAttribute(Xml.LEG2_OPTIONS)) != null) {
4204            try {
4205                _leg2Options = Integer.parseInt(a.getValue());
4206            } catch (NumberFormatException ee) {
4207                log.error("Leg 2 options ({}) isn't a valid number for train ({})", a.getValue(), getName());
4208            }
4209        }
4210        if ((a = e.getAttribute(Xml.LEG3_OPTIONS)) != null) {
4211            try {
4212                _leg3Options = Integer.parseInt(a.getValue());
4213            } catch (NumberFormatException ee) {
4214                log.error("Leg 3 options ({}) isn't a valid number for train ({})", a.getValue(), getName());
4215            }
4216        }
4217        if ((a = e.getAttribute(Xml.BUILD_NORMAL)) != null) {
4218            _buildNormal = a.getValue().equals(Xml.TRUE);
4219        }
4220        if ((a = e.getAttribute(Xml.TO_TERMINAL)) != null) {
4221            _sendToTerminal = a.getValue().equals(Xml.TRUE);
4222        }
4223        if ((a = e.getAttribute(Xml.ALLOW_LOCAL_MOVES)) != null) {
4224            _allowLocalMoves = a.getValue().equals(Xml.TRUE);
4225        }
4226        if ((a = e.getAttribute(Xml.ALLOW_THROUGH_CARS)) != null) {
4227            _allowThroughCars = a.getValue().equals(Xml.TRUE);
4228        }
4229        if ((a = e.getAttribute(Xml.ALLOW_RETURN)) != null) {
4230            _allowCarsReturnStaging = a.getValue().equals(Xml.TRUE);
4231        }
4232        if ((a = e.getAttribute(Xml.SERVICE_ALL)) != null) {
4233            _serviceAllCarsWithFinalDestinations = a.getValue().equals(Xml.TRUE);
4234        }
4235        if ((a = e.getAttribute(Xml.BUILD_CONSIST)) != null) {
4236            _buildConsist = a.getValue().equals(Xml.TRUE);
4237        }
4238        if ((a = e.getAttribute(Xml.SEND_CUSTOM_STAGING)) != null) {
4239            _sendCarsWithCustomLoadsToStaging = a.getValue().equals(Xml.TRUE);
4240        }
4241        if ((a = e.getAttribute(Xml.BUILT)) != null) {
4242            _built = a.getValue().equals(Xml.TRUE);
4243        }
4244        if ((a = e.getAttribute(Xml.BUILD)) != null) {
4245            _build = a.getValue().equals(Xml.TRUE);
4246        }
4247        if ((a = e.getAttribute(Xml.BUILD_FAILED)) != null) {
4248            _buildFailed = a.getValue().equals(Xml.TRUE);
4249        }
4250        if ((a = e.getAttribute(Xml.BUILD_FAILED_MESSAGE)) != null) {
4251            _buildFailedMessage = a.getValue();
4252        }
4253        if ((a = e.getAttribute(Xml.PRINTED)) != null) {
4254            _printed = a.getValue().equals(Xml.TRUE);
4255        }
4256        if ((a = e.getAttribute(Xml.MODIFIED)) != null) {
4257            _modified = a.getValue().equals(Xml.TRUE);
4258        }
4259        if ((a = e.getAttribute(Xml.SWITCH_LIST_STATUS)) != null) {
4260            _switchListStatus = a.getValue();
4261        }
4262        if ((a = e.getAttribute(Xml.LEAD_ENGINE)) != null) {
4263            _leadEngineId = a.getValue();
4264        }
4265        if ((a = e.getAttribute(Xml.TERMINATION_DATE)) != null) {
4266            _date = TrainCommon.convertStringToLocalDateTime(a.getValue());
4267        }
4268        if ((a = e.getAttribute(Xml.REQUESTED_CARS)) != null) {
4269            try {
4270                _statusCarsRequested = Integer.parseInt(a.getValue());
4271            } catch (NumberFormatException ee) {
4272                log.error("Status cars requested ({}) isn't a valid number for train ({})", a.getValue(), getName());
4273            }
4274        }
4275        if ((a = e.getAttribute(Xml.STATUS_CODE)) != null) {
4276            try {
4277                _statusCode = Integer.parseInt(a.getValue());
4278            } catch (NumberFormatException ee) {
4279                log.error("Status code ({}) isn't a valid number for train ({})", a.getValue(), getName());
4280            }
4281        } else if ((a = e.getAttribute(Xml.STATUS)) != null) {
4282            // attempt to recover status code
4283            String status = a.getValue();
4284            if (status.startsWith(BUILD_FAILED)) {
4285                _statusCode = CODE_BUILD_FAILED;
4286            } else if (status.startsWith(BUILT)) {
4287                _statusCode = CODE_BUILT;
4288            } else if (status.startsWith(PARTIAL_BUILT)) {
4289                _statusCode = CODE_PARTIAL_BUILT;
4290            } else if (status.startsWith(TERMINATED)) {
4291                _statusCode = CODE_TERMINATED;
4292            } else if (status.startsWith(TRAIN_EN_ROUTE)) {
4293                _statusCode = CODE_TRAIN_EN_ROUTE;
4294            } else if (status.startsWith(TRAIN_RESET)) {
4295                _statusCode = CODE_TRAIN_RESET;
4296            } else {
4297                _statusCode = CODE_UNKNOWN;
4298            }
4299        }
4300        if ((a = e.getAttribute(Xml.OLD_STATUS_CODE)) != null) {
4301            try {
4302                _oldStatusCode = Integer.parseInt(a.getValue());
4303            } catch (NumberFormatException ee) {
4304                log.error("Old status code ({}) isn't a valid number for train ({})", a.getValue(), getName());
4305            }
4306        } else {
4307            _oldStatusCode = getStatusCode(); // use current status code if one
4308                                              // wasn't saved
4309        }
4310        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
4311            _comment = a.getValue();
4312        }
4313        if (getRoute() != null) {
4314            if ((a = e.getAttribute(Xml.CURRENT)) != null) {
4315                _current = getRoute().getRouteLocationById(a.getValue());
4316            }
4317            if ((a = e.getAttribute(Xml.LEG2_START)) != null) {
4318                _leg2Start = getRoute().getRouteLocationById(a.getValue());
4319            }
4320            if ((a = e.getAttribute(Xml.LEG3_START)) != null) {
4321                _leg3Start = getRoute().getRouteLocationById(a.getValue());
4322            }
4323            if ((a = e.getAttribute(Xml.LEG2_END)) != null) {
4324                _end2Leg = getRoute().getRouteLocationById(a.getValue());
4325            }
4326            if ((a = e.getAttribute(Xml.LEG3_END)) != null) {
4327                _leg3End = getRoute().getRouteLocationById(a.getValue());
4328            }
4329            if ((a = e.getAttribute(Xml.DEPARTURE_TRACK)) != null) {
4330                Location location = InstanceManager.getDefault(LocationManager.class)
4331                        .getLocationByName(getTrainDepartsName());
4332                if (location != null) {
4333                    _departureTrack = location.getTrackById(a.getValue());
4334                } else {
4335                    log.error("Departure location not found for track {}", a.getValue());
4336                }
4337            }
4338            if ((a = e.getAttribute(Xml.TERMINATION_TRACK)) != null) {
4339                Location location = InstanceManager.getDefault(LocationManager.class)
4340                        .getLocationByName(getTrainTerminatesName());
4341                if (location != null) {
4342                    _terminationTrack = location.getTrackById(a.getValue());
4343                } else {
4344                    log.error("Termiation location not found for track {}", a.getValue());
4345                }
4346            }
4347        }
4348
4349        // check for scripts
4350        if (e.getChild(Xml.SCRIPTS) != null) {
4351            List<Element> lb = e.getChild(Xml.SCRIPTS).getChildren(Xml.BUILD);
4352            for (Element es : lb) {
4353                if ((a = es.getAttribute(Xml.NAME)) != null) {
4354                    addBuildScript(a.getValue());
4355                }
4356            }
4357            List<Element> lab = e.getChild(Xml.SCRIPTS).getChildren(Xml.AFTER_BUILD);
4358            for (Element es : lab) {
4359                if ((a = es.getAttribute(Xml.NAME)) != null) {
4360                    addAfterBuildScript(a.getValue());
4361                }
4362            }
4363            List<Element> lm = e.getChild(Xml.SCRIPTS).getChildren(Xml.MOVE);
4364            for (Element es : lm) {
4365                if ((a = es.getAttribute(Xml.NAME)) != null) {
4366                    addMoveScript(a.getValue());
4367                }
4368            }
4369            List<Element> lt = e.getChild(Xml.SCRIPTS).getChildren(Xml.TERMINATE);
4370            for (Element es : lt) {
4371                if ((a = es.getAttribute(Xml.NAME)) != null) {
4372                    addTerminationScript(a.getValue());
4373                }
4374            }
4375        }
4376        // check for optional railroad name and logo
4377        if ((e.getChild(Xml.RAIL_ROAD) != null) && (a = e.getChild(Xml.RAIL_ROAD).getAttribute(Xml.NAME)) != null) {
4378            String name = a.getValue();
4379            setRailroadName(name);
4380        }
4381        if ((e.getChild(Xml.MANIFEST_LOGO) != null)) {
4382            if ((a = e.getChild(Xml.MANIFEST_LOGO).getAttribute(Xml.NAME)) != null) {
4383                setManifestLogoPathName(a.getValue());
4384            }
4385        }
4386        if ((a = e.getAttribute(Xml.SHOW_TIMES)) != null) {
4387            _showTimes = a.getValue().equals(Xml.TRUE);
4388        }
4389
4390        addPropertyChangeListerners();
4391    }
4392
4393    private void addPropertyChangeListerners() {
4394        InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
4395        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
4396        InstanceManager.getDefault(EngineTypes.class).addPropertyChangeListener(this);
4397        InstanceManager.getDefault(CarOwners.class).addPropertyChangeListener(this);
4398        InstanceManager.getDefault(EngineModels.class).addPropertyChangeListener(this);
4399    }
4400
4401    /**
4402     * Create an XML element to represent this Entry. This member has to remain
4403     * synchronized with the detailed DTD in operations-trains.dtd.
4404     *
4405     * @return Contents in a JDOM Element
4406     */
4407    public Element store() {
4408        Element e = new Element(Xml.TRAIN);
4409        e.setAttribute(Xml.ID, getId());
4410        e.setAttribute(Xml.NAME, getName());
4411        e.setAttribute(Xml.DESCRIPTION, getRawDescription());
4412        e.setAttribute(Xml.DEPART_DAY, getDepartureTimeDay());
4413        e.setAttribute(Xml.DEPART_HOUR, getDepartureTimeHour());
4414        e.setAttribute(Xml.DEPART_MINUTE, getDepartureTimeMinute());
4415
4416        Element eRowColor = new Element(Xml.ROW_COLOR);
4417        eRowColor.setAttribute(Xml.NAME, getTableRowColorName());
4418        eRowColor.setAttribute(Xml.RESET_ROW_COLOR, getTableRowColorNameReset());
4419        e.addContent(eRowColor);
4420
4421        Element eRoute = new Element(Xml.ROUTE);
4422        if (getRoute() != null) {
4423            eRoute.setAttribute(Xml.NAME, getRoute().getName());
4424            eRoute.setAttribute(Xml.ID, getRoute().getId());
4425            e.addContent(eRoute);
4426            // build list of locations that this train skips
4427            String[] locationIds = getTrainSkipsLocations();
4428            if (locationIds.length > 0) {
4429                Element eSkips = new Element(Xml.SKIPS);
4430                for (String id : locationIds) {
4431                    Element eLoc = new Element(Xml.LOCATION);
4432                    RouteLocation rl = getRoute().getRouteLocationById(id);
4433                    if (rl != null) {
4434                        eLoc.setAttribute(Xml.NAME, rl.getName());
4435                        eLoc.setAttribute(Xml.ID, id);
4436                        eSkips.addContent(eLoc);
4437                    }
4438                }
4439                eRoute.addContent(eSkips);
4440            }
4441        }
4442        // build list of locations that this train skips
4443        if (getCurrentRouteLocation() != null) {
4444            e.setAttribute(Xml.CURRENT, getCurrentRouteLocation().getId());
4445        }
4446        if (getDepartureTrack() != null) {
4447            e.setAttribute(Xml.DEPARTURE_TRACK, getDepartureTrack().getId());
4448        }
4449        if (getTerminationTrack() != null) {
4450            e.setAttribute(Xml.TERMINATION_TRACK, getTerminationTrack().getId());
4451        }
4452        e.setAttribute(Xml.BUILT_START_YEAR, getBuiltStartYear());
4453        e.setAttribute(Xml.BUILT_END_YEAR, getBuiltEndYear());
4454        e.setAttribute(Xml.NUMBER_ENGINES, getNumberEngines());
4455        e.setAttribute(Xml.ENGINE_ROAD, getEngineRoad());
4456        e.setAttribute(Xml.ENGINE_MODEL, getEngineModel());
4457        e.setAttribute(Xml.REQUIRES, Integer.toString(getRequirements()));
4458        e.setAttribute(Xml.CABOOSE_ROAD, getCabooseRoad());
4459        e.setAttribute(Xml.BUILD_NORMAL, isBuildTrainNormalEnabled() ? Xml.TRUE : Xml.FALSE);
4460        e.setAttribute(Xml.TO_TERMINAL, isSendCarsToTerminalEnabled() ? Xml.TRUE : Xml.FALSE);
4461        e.setAttribute(Xml.ALLOW_LOCAL_MOVES, isAllowLocalMovesEnabled() ? Xml.TRUE : Xml.FALSE);
4462        e.setAttribute(Xml.ALLOW_RETURN, isAllowReturnToStagingEnabled() ? Xml.TRUE : Xml.FALSE);
4463        e.setAttribute(Xml.ALLOW_THROUGH_CARS, isAllowThroughCarsEnabled() ? Xml.TRUE : Xml.FALSE);
4464        e.setAttribute(Xml.SERVICE_ALL, isServiceAllCarsWithFinalDestinationsEnabled() ? Xml.TRUE : Xml.FALSE);
4465        e.setAttribute(Xml.SEND_CUSTOM_STAGING, isSendCarsWithCustomLoadsToStagingEnabled() ? Xml.TRUE : Xml.FALSE);
4466        e.setAttribute(Xml.BUILD_CONSIST, isBuildConsistEnabled() ? Xml.TRUE : Xml.FALSE);
4467        e.setAttribute(Xml.BUILT, isBuilt() ? Xml.TRUE : Xml.FALSE);
4468        e.setAttribute(Xml.BUILD, isBuildEnabled() ? Xml.TRUE : Xml.FALSE);
4469        e.setAttribute(Xml.BUILD_FAILED, isBuildFailed() ? Xml.TRUE : Xml.FALSE);
4470        e.setAttribute(Xml.BUILD_FAILED_MESSAGE, getBuildFailedMessage());
4471        e.setAttribute(Xml.PRINTED, isPrinted() ? Xml.TRUE : Xml.FALSE);
4472        e.setAttribute(Xml.MODIFIED, isModified() ? Xml.TRUE : Xml.FALSE);
4473        e.setAttribute(Xml.SWITCH_LIST_STATUS, getSwitchListStatus());
4474        if (getLeadEngine() != null) {
4475            e.setAttribute(Xml.LEAD_ENGINE, getLeadEngine().getId());
4476        }
4477        e.setAttribute(Xml.STATUS, getStatus());
4478        e.setAttribute(Xml.TERMINATION_DATE, getDate());
4479        e.setAttribute(Xml.REQUESTED_CARS, Integer.toString(getNumberCarsRequested()));
4480        e.setAttribute(Xml.STATUS_CODE, Integer.toString(getStatusCode()));
4481        e.setAttribute(Xml.OLD_STATUS_CODE, Integer.toString(getOldStatusCode()));
4482        e.setAttribute(Xml.COMMENT, getCommentWithColor());
4483        e.setAttribute(Xml.SHOW_TIMES, isShowArrivalAndDepartureTimesEnabled() ? Xml.TRUE : Xml.FALSE);
4484        // build list of car types for this train
4485        String[] types = getTypeNames();
4486        // new way of saving car types
4487        Element eTypes = new Element(Xml.TYPES);
4488        for (String type : types) {
4489            // don't save types that have been deleted by user
4490            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
4491                Element eType = new Element(Xml.LOCO_TYPE);
4492                eType.setAttribute(Xml.NAME, type);
4493                eTypes.addContent(eType);
4494            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
4495                Element eType = new Element(Xml.CAR_TYPE);
4496                eType.setAttribute(Xml.NAME, type);
4497                eTypes.addContent(eType);
4498            }
4499        }
4500        e.addContent(eTypes);
4501        // save list of car roads for this train
4502        if (!getCarRoadOption().equals(ALL_ROADS)) {
4503            e.setAttribute(Xml.CAR_ROAD_OPTION, getCarRoadOption());
4504            String[] roads = getCarRoadNames();
4505            // new way of saving road names
4506            Element eRoads = new Element(Xml.CAR_ROADS);
4507            for (String road : roads) {
4508                Element eRoad = new Element(Xml.CAR_ROAD);
4509                eRoad.setAttribute(Xml.NAME, road);
4510                eRoads.addContent(eRoad);
4511            }
4512            e.addContent(eRoads);
4513        }
4514        // save list of caboose roads for this train
4515        if (!getCabooseRoadOption().equals(ALL_ROADS)) {
4516            e.setAttribute(Xml.CABOOSE_ROAD_OPTION, getCabooseRoadOption());
4517            String[] roads = getCabooseRoadNames();
4518            // new way of saving road names
4519            Element eRoads = new Element(Xml.CABOOSE_ROADS);
4520            for (String road : roads) {
4521                Element eRoad = new Element(Xml.CAR_ROAD);
4522                eRoad.setAttribute(Xml.NAME, road);
4523                eRoads.addContent(eRoad);
4524            }
4525            e.addContent(eRoads);
4526        }
4527        // save list of engine roads for this train
4528        if (!getLocoRoadOption().equals(ALL_ROADS)) {
4529            e.setAttribute(Xml.LOCO_ROAD_OPTION, getLocoRoadOption());
4530            String[] roads = getLocoRoadNames();
4531            Element eRoads = new Element(Xml.LOCO_ROADS);
4532            for (String road : roads) {
4533                Element eRoad = new Element(Xml.LOCO_ROAD);
4534                eRoad.setAttribute(Xml.NAME, road);
4535                eRoads.addContent(eRoad);
4536            }
4537            e.addContent(eRoads);
4538        }
4539        // save list of car loads for this train
4540        if (!getLoadOption().equals(ALL_LOADS)) {
4541            e.setAttribute(Xml.CAR_LOAD_OPTION, getLoadOption());
4542            String[] loads = getLoadNames();
4543            // new way of saving car loads
4544            Element eLoads = new Element(Xml.CAR_LOADS);
4545            for (String load : loads) {
4546                Element eLoad = new Element(Xml.CAR_LOAD);
4547                eLoad.setAttribute(Xml.NAME, load);
4548                eLoads.addContent(eLoad);
4549            }
4550            e.addContent(eLoads);
4551        }
4552        // save list of car owners for this train
4553        if (!getOwnerOption().equals(ALL_OWNERS)) {
4554            e.setAttribute(Xml.CAR_OWNER_OPTION, getOwnerOption());
4555            String[] owners = getOwnerNames();
4556            // new way of saving car owners
4557            Element eOwners = new Element(Xml.CAR_OWNERS);
4558            for (String owner : owners) {
4559                Element eOwner = new Element(Xml.CAR_OWNER);
4560                eOwner.setAttribute(Xml.NAME, owner);
4561                eOwners.addContent(eOwner);
4562            }
4563            e.addContent(eOwners);
4564        }
4565        // save list of scripts for this train
4566        if (getBuildScripts().size() > 0 ||
4567                getAfterBuildScripts().size() > 0 ||
4568                getMoveScripts().size() > 0 ||
4569                getTerminationScripts().size() > 0) {
4570            Element es = new Element(Xml.SCRIPTS);
4571            if (getBuildScripts().size() > 0) {
4572                for (String scriptPathname : getBuildScripts()) {
4573                    Element em = new Element(Xml.BUILD);
4574                    em.setAttribute(Xml.NAME, scriptPathname);
4575                    es.addContent(em);
4576                }
4577            }
4578            if (getAfterBuildScripts().size() > 0) {
4579                for (String scriptPathname : getAfterBuildScripts()) {
4580                    Element em = new Element(Xml.AFTER_BUILD);
4581                    em.setAttribute(Xml.NAME, scriptPathname);
4582                    es.addContent(em);
4583                }
4584            }
4585            if (getMoveScripts().size() > 0) {
4586                for (String scriptPathname : getMoveScripts()) {
4587                    Element em = new Element(Xml.MOVE);
4588                    em.setAttribute(Xml.NAME, scriptPathname);
4589                    es.addContent(em);
4590                }
4591            }
4592            // save list of termination scripts for this train
4593            if (getTerminationScripts().size() > 0) {
4594                for (String scriptPathname : getTerminationScripts()) {
4595                    Element et = new Element(Xml.TERMINATE);
4596                    et.setAttribute(Xml.NAME, scriptPathname);
4597                    es.addContent(et);
4598                }
4599            }
4600            e.addContent(es);
4601        }
4602        if (!getRailroadName().equals(NONE)) {
4603            Element r = new Element(Xml.RAIL_ROAD);
4604            r.setAttribute(Xml.NAME, getRailroadName());
4605            e.addContent(r);
4606        }
4607        if (!getManifestLogoPathName().equals(NONE)) {
4608            Element l = new Element(Xml.MANIFEST_LOGO);
4609            l.setAttribute(Xml.NAME, getManifestLogoPathName());
4610            e.addContent(l);
4611        }
4612
4613        if (getSecondLegOptions() != NO_CABOOSE_OR_FRED) {
4614            e.setAttribute(Xml.LEG2_OPTIONS, Integer.toString(getSecondLegOptions()));
4615            e.setAttribute(Xml.LEG2_ENGINES, getSecondLegNumberEngines());
4616            e.setAttribute(Xml.LEG2_ROAD, getSecondLegEngineRoad());
4617            e.setAttribute(Xml.LEG2_MODEL, getSecondLegEngineModel());
4618            e.setAttribute(Xml.LEG2_CABOOSE_ROAD, getSecondLegCabooseRoad());
4619            if (getSecondLegStartRouteLocation() != null) {
4620                e.setAttribute(Xml.LEG2_START, getSecondLegStartRouteLocation().getId());
4621            }
4622            if (getSecondLegEndRouteLocation() != null) {
4623                e.setAttribute(Xml.LEG2_END, getSecondLegEndRouteLocation().getId());
4624            }
4625        }
4626        if (getThirdLegOptions() != NO_CABOOSE_OR_FRED) {
4627            e.setAttribute(Xml.LEG3_OPTIONS, Integer.toString(getThirdLegOptions()));
4628            e.setAttribute(Xml.LEG3_ENGINES, getThirdLegNumberEngines());
4629            e.setAttribute(Xml.LEG3_ROAD, getThirdLegEngineRoad());
4630            e.setAttribute(Xml.LEG3_MODEL, getThirdLegEngineModel());
4631            e.setAttribute(Xml.LEG3_CABOOSE_ROAD, getThirdLegCabooseRoad());
4632            if (getThirdLegStartRouteLocation() != null) {
4633                e.setAttribute(Xml.LEG3_START, getThirdLegStartRouteLocation().getId());
4634            }
4635            if (getThirdLegEndRouteLocation() != null) {
4636                e.setAttribute(Xml.LEG3_END, getThirdLegEndRouteLocation().getId());
4637            }
4638        }
4639        return e;
4640    }
4641
4642    @Override
4643    public void propertyChange(java.beans.PropertyChangeEvent e) {
4644        if (Control.SHOW_PROPERTY) {
4645            log.debug("Train ({}) sees property change: ({}) old: ({}) new: ({})", getName(), e.getPropertyName(),
4646                    e.getOldValue(), e.getNewValue());
4647        }
4648        if (e.getPropertyName().equals(Route.DISPOSE)) {
4649            setRoute(null);
4650        }
4651        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY) ||
4652                e.getPropertyName().equals(CarTypes.CARTYPES_CHANGED_PROPERTY) ||
4653                e.getPropertyName().equals(EngineTypes.ENGINETYPES_NAME_CHANGED_PROPERTY)) {
4654            replaceType((String) e.getOldValue(), (String) e.getNewValue());
4655        }
4656        if (e.getPropertyName().equals(CarRoads.CARROADS_NAME_CHANGED_PROPERTY)) {
4657            replaceRoad((String) e.getOldValue(), (String) e.getNewValue());
4658        }
4659        if (e.getPropertyName().equals(CarOwners.CAROWNERS_NAME_CHANGED_PROPERTY)) {
4660            replaceOwner((String) e.getOldValue(), (String) e.getNewValue());
4661        }
4662        if (e.getPropertyName().equals(EngineModels.ENGINEMODELS_NAME_CHANGED_PROPERTY)) {
4663            replaceModel((String) e.getOldValue(), (String) e.getNewValue());
4664        }
4665        // forward route departure time property changes
4666        if (e.getPropertyName().equals(RouteLocation.DEPARTURE_TIME_CHANGED_PROPERTY)) {
4667            setDirtyAndFirePropertyChange(DEPARTURETIME_CHANGED_PROPERTY, e.getOldValue(), e.getNewValue());
4668        }
4669        // forward any property changes in this train's route
4670        if (e.getSource().getClass().equals(Route.class)) {
4671            setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
4672        }
4673    }
4674
4675    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
4676        InstanceManager.getDefault(TrainManagerXml.class).setDirty(true);
4677        firePropertyChange(p, old, n);
4678    }
4679
4680    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Train.class);
4681
4682}