java/src/jmri/jmrit/operations/trains/Train.java

Summary

Maintainability
F
1 mo
Test Coverage
B
83%
package jmri.jmrit.operations.trains;

import java.awt.Color;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.text.MessageFormat;
import java.util.*;

import org.jdom2.Element;

import jmri.InstanceManager;
import jmri.beans.Identifiable;
import jmri.beans.PropertyChangeSupport;
import jmri.jmrit.display.Editor;
import jmri.jmrit.display.EditorManager;
import jmri.jmrit.operations.locations.*;
import jmri.jmrit.operations.rollingstock.RollingStock;
import jmri.jmrit.operations.rollingstock.RollingStockManager;
import jmri.jmrit.operations.rollingstock.cars.*;
import jmri.jmrit.operations.rollingstock.engines.*;
import jmri.jmrit.operations.routes.*;
import jmri.jmrit.operations.setup.Control;
import jmri.jmrit.operations.setup.Setup;
import jmri.jmrit.operations.trains.excel.TrainCustomManifest;
import jmri.jmrit.roster.RosterEntry;
import jmri.script.JmriScriptEngineManager;
import jmri.util.FileUtil;
import jmri.util.swing.JmriJOptionPane;

/**
 * Represents a train on the layout
 *
 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013,
 *         2014, 2015
 * @author Rodney Black Copyright (C) 2011
 */
public class Train extends PropertyChangeSupport implements Identifiable, PropertyChangeListener {

    /*
     * WARNING DO NOT LOAD CAR OR ENGINE MANAGERS WHEN Train.java IS CREATED IT
     * CAUSES A RECURSIVE LOOP AT LOAD TIME, SEE EXAMPLES BELOW CarManager
     * carManager = InstanceManager.getDefault(CarManager.class); EngineManager
     * engineManager = InstanceManager.getDefault(EngineManager.class);
     */

    // The release date for JMRI operations 10/29/2008

    public static final String NONE = "";

    protected String _id = NONE;
    protected String _name = NONE;
    protected String _description = NONE;
    protected RouteLocation _current = null;// where the train is located in its route
    protected String _buildFailedMessage = NONE; // the build failed message for this train
    protected boolean _built = false; // when true, a train manifest has been built
    protected boolean _modified = false; // when true, user has modified train after being built
    protected boolean _build = true; // when true, build this train
    protected boolean _buildFailed = false; // when true, build for this train failed
    protected boolean _printed = false; // when true, manifest has been printed
    protected boolean _sendToTerminal = false; // when true, cars picked up by train only go to terminal
    protected boolean _allowLocalMoves = true; // when true, cars with custom loads can be moved locally
    protected boolean _allowThroughCars = true; // when true, cars from the origin can be sent to the terminal
    protected boolean _buildNormal = false; // when true build this train in normal mode
    protected boolean _allowCarsReturnStaging = false; // when true allow cars to return to staging
    protected boolean _serviceAllCarsWithFinalDestinations = false; // when true, service cars with final destinations
    protected boolean _buildConsist = false; // when true, build a consist for this train using single locomotives
    protected boolean _sendCarsWithCustomLoadsToStaging = false; // when true, send cars to staging if spurs full
    protected Route _route = null;
    protected Track _departureTrack; // the departure track from staging
    protected Track _terminationTrack; // the termination track into staging
    protected String _carRoadOption = ALL_ROADS;// train car road name restrictions
    protected String _locoRoadOption = ALL_ROADS;// train engine road name restrictions
    protected int _requires = NO_CABOOSE_OR_FRED; // train requirements, caboose, FRED
    protected String _numberEngines = "0"; // number of engines this train requires
    protected String _engineRoad = NONE; // required road name for engines assigned to this train
    protected String _engineModel = NONE; // required model of engines assigned to this train
    protected String _cabooseRoad = NONE; // required road name for cabooses assigned to this train
    protected String _departureTime = "00:00"; // NOI18N departure time for this train
    protected String _leadEngineId = NONE; // lead engine for train icon info
    protected String _builtStartYear = NONE; // built start year
    protected String _builtEndYear = NONE; // built end year
    protected String _loadOption = ALL_LOADS;// train load restrictions
    protected String _ownerOption = ALL_OWNERS;// train owner name restrictions
    protected List<String> _buildScripts = new ArrayList<>(); // list of script pathnames to run before train is built
    protected List<String> _afterBuildScripts = new ArrayList<>(); // list of script pathnames to run after train is
                                                                   // built
    protected List<String> _moveScripts = new ArrayList<>(); // list of script pathnames to run when train is moved
    protected List<String> _terminationScripts = new ArrayList<>(); // list of script pathnames to run when train is
                                                                    // terminated
    protected String _railroadName = NONE; // optional railroad name for this train
    protected String _logoPathName = NONE; // optional manifest logo for this train
    protected boolean _showTimes = true; // when true, show arrival and departure times for this train
    protected Engine _leadEngine = null; // lead engine for icon
    protected String _switchListStatus = UNKNOWN; // print switch list status
    protected String _comment = NONE;
    protected String _serviceStatus = NONE; // status only if train is being built
    protected int _statusCode = CODE_UNKNOWN;
    protected int _oldStatusCode = CODE_UNKNOWN;
    protected String _statusTerminatedDate = NONE;
    protected int _statusCarsRequested = 0;
    protected String _tableRowColorName = NONE; // color of row in Trains table
    protected String _tableRowColorResetName = NONE; // color of row in Trains table when reset

    // Engine change and helper engines
    protected int _leg2Options = NO_CABOOSE_OR_FRED; // options
    protected RouteLocation _leg2Start = null; // route location when 2nd leg begins
    protected RouteLocation _end2Leg = null; // route location where 2nd leg ends
    protected String _leg2Engines = "0"; // number of engines 2nd leg
    protected String _leg2Road = NONE; // engine road name 2nd leg
    protected String _leg2Model = NONE; // engine model 2nd leg
    protected String _leg2CabooseRoad = NONE; // road name for caboose 2nd leg

    protected int _leg3Options = NO_CABOOSE_OR_FRED; // options
    protected RouteLocation _leg3Start = null; // route location when 3rd leg begins
    protected RouteLocation _leg3End = null; // route location where 3rd leg ends
    protected String _leg3Engines = "0"; // number of engines 3rd leg
    protected String _leg3Road = NONE; // engine road name 3rd leg
    protected String _leg3Model = NONE; // engine model 3rd leg
    protected String _leg3CabooseRoad = NONE; // road name for caboose 3rd leg

    // engine change and helper options
    public static final int CHANGE_ENGINES = 1; // change engines
    public static final int HELPER_ENGINES = 2; // add helper engines
    public static final int ADD_CABOOSE = 4; // add caboose
    public static final int REMOVE_CABOOSE = 8; // remove caboose

    // property change names
    public static final String DISPOSE_CHANGED_PROPERTY = "TrainDispose"; // NOI18N
    public static final String STOPS_CHANGED_PROPERTY = "TrainStops"; // NOI18N
    public static final String TYPES_CHANGED_PROPERTY = "TrainTypes"; // NOI18N
    public static final String BUILT_CHANGED_PROPERTY = "TrainBuilt"; // NOI18N
    public static final String BUILT_YEAR_CHANGED_PROPERTY = "TrainBuiltYear"; // NOI18N
    public static final String BUILD_CHANGED_PROPERTY = "TrainBuild"; // NOI18N
    public static final String ROADS_CHANGED_PROPERTY = "TrainRoads"; // NOI18N
    public static final String LOADS_CHANGED_PROPERTY = "TrainLoads"; // NOI18N
    public static final String OWNERS_CHANGED_PROPERTY = "TrainOwners"; // NOI18N
    public static final String NAME_CHANGED_PROPERTY = "TrainName"; // NOI18N
    public static final String DESCRIPTION_CHANGED_PROPERTY = "TrainDescription"; // NOI18N
    public static final String STATUS_CHANGED_PROPERTY = "TrainStatus"; // NOI18N
    public static final String DEPARTURETIME_CHANGED_PROPERTY = "TrainDepartureTime"; // NOI18N
    public static final String TRAIN_LOCATION_CHANGED_PROPERTY = "TrainLocation"; // NOI18N
    public static final String TRAIN_ROUTE_CHANGED_PROPERTY = "TrainRoute"; // NOI18N
    public static final String TRAIN_REQUIREMENTS_CHANGED_PROPERTY = "TrainRequirements"; // NOI18N
    public static final String TRAIN_MOVE_COMPLETE_CHANGED_PROPERTY = "TrainMoveComplete"; // NOI18N
    public static final String TRAIN_ROW_COLOR_CHANGED_PROPERTY = "TrianRowColor"; // NOI18N
    public static final String TRAIN_ROW_COLOR_RESET_CHANGED_PROPERTY = "TrianRowColorReset"; // NOI18N
    public static final String TRAIN_MODIFIED_CHANGED_PROPERTY = "TrainModified"; // NOI18N

    // Train status
    public static final String TRAIN_RESET = Bundle.getMessage("TrainReset");
    public static final String RUN_SCRIPTS = Bundle.getMessage("RunScripts");
    public static final String BUILDING = Bundle.getMessage("Building");
    public static final String BUILD_FAILED = Bundle.getMessage("BuildFailed");
    public static final String BUILT = Bundle.getMessage("Built");
    public static final String PARTIAL_BUILT = Bundle.getMessage("Partial");
    public static final String TRAIN_EN_ROUTE = Bundle.getMessage("TrainEnRoute");
    public static final String TERMINATED = Bundle.getMessage("Terminated");
    public static final String MANIFEST_MODIFIED = Bundle.getMessage("Modified");

    // Train status codes
    public static final int CODE_TRAIN_RESET = 0;
    public static final int CODE_RUN_SCRIPTS = 0x100;
    public static final int CODE_BUILDING = 0x01;
    public static final int CODE_BUILD_FAILED = 0x02;
    public static final int CODE_BUILT = 0x10;
    public static final int CODE_PARTIAL_BUILT = CODE_BUILT + 0x04;
    public static final int CODE_TRAIN_EN_ROUTE = CODE_BUILT + 0x08;
    public static final int CODE_TERMINATED = 0x80;
    public static final int CODE_MANIFEST_MODIFIED = 0x200;
    public static final int CODE_UNKNOWN = 0xFFFF;

    // train requirements
    public static final int NO_CABOOSE_OR_FRED = 0; // default
    public static final int CABOOSE = 1;
    public static final int FRED = 2;

    // road options
    public static final String ALL_ROADS = Bundle.getMessage("All");
    public static final String INCLUDE_ROADS = Bundle.getMessage("Include");
    public static final String EXCLUDE_ROADS = Bundle.getMessage("Exclude");

    // owner options
    public static final String ALL_OWNERS = Bundle.getMessage("All");
    public static final String INCLUDE_OWNERS = Bundle.getMessage("Include");
    public static final String EXCLUDE_OWNERS = Bundle.getMessage("Exclude");

    // load options
    public static final String ALL_LOADS = Bundle.getMessage("All");
    public static final String INCLUDE_LOADS = Bundle.getMessage("Include");
    public static final String EXCLUDE_LOADS = Bundle.getMessage("Exclude");

    // Switch list status
    public static final String UNKNOWN = "";
    public static final String PRINTED = Bundle.getMessage("Printed");

    public static final String AUTO = Bundle.getMessage("Auto");
    public static final String AUTO_HPT = Bundle.getMessage("AutoHPT");

    public Train(String id, String name) {
        log.debug("New train ({}) id: {}", name, id);
        _name = name;
        _id = id;
        // a new train accepts all types
        setTypeNames(InstanceManager.getDefault(CarTypes.class).getNames());
        setTypeNames(InstanceManager.getDefault(EngineTypes.class).getNames());
        addPropertyChangeListerners();
    }

    @Override
    public String getId() {
        return _id;
    }

    /**
     * Sets the name of this train, normally a short name that can fit within the
     * train icon.
     *
     * @param name the train's name.
     */
    public void setName(String name) {
        String old = _name;
        _name = name;
        if (!old.equals(name)) {
            setDirtyAndFirePropertyChange(NAME_CHANGED_PROPERTY, old, name);
        }
    }

    // for combo boxes
    /**
     * Get's a train's name
     *
     * @return train's name
     */
    @Override
    public String toString() {
        return _name;
    }

    /**
     * Get's a train's name
     *
     * @return train's name
     */
    public String getName() {
        return _name;
    }

    /**
     * @return The name of the color when highlighting the train's row
     */
    public String getTableRowColorName() {
        return _tableRowColorName;
    }

    public void setTableRowColorName(String colorName) {
        String old = _tableRowColorName;
        _tableRowColorName = colorName;
        if (!old.equals(colorName)) {
            setDirtyAndFirePropertyChange(TRAIN_ROW_COLOR_CHANGED_PROPERTY, old, colorName);
        }
    }

    /**
     * @return The name of the train row color when the train is reset
     */
    public String getTableRowColorNameReset() {
        return _tableRowColorResetName;
    }

    public void setTableRowColorNameReset(String colorName) {
        String old = _tableRowColorResetName;
        _tableRowColorResetName = colorName;
        if (!old.equals(colorName)) {
            setDirtyAndFirePropertyChange(TRAIN_ROW_COLOR_RESET_CHANGED_PROPERTY, old, colorName);
        }
    }

    /**
     * @return The color when highlighting the train's row
     */
    public Color getTableRowColor() {
        String colorName = getTableRowColorName();
        if (colorName.equals(NONE)) {
            return null;
        } else {
            return Setup.getColor(colorName);
        }
    }

    /**
     * Get's train's departure time
     *
     * @return train's departure time in the String format hh:mm
     */
    public String getDepartureTime() {
        // check to see if the route has a departure time
        RouteLocation rl = getTrainDepartsRouteLocation();
        if (rl != null) {
            rl.removePropertyChangeListener(this);
            rl.addPropertyChangeListener(this);
            if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
                return rl.getDepartureTime();
            }
        }
        return _departureTime;
    }

    /**
     * Get's train's departure time in 12hr or 24hr format
     *
     * @return train's departure time in the String format hh:mm or hh:mm(AM/PM)
     */
    public String getFormatedDepartureTime() {
        // check to see if the route has a departure time
        RouteLocation rl = getTrainDepartsRouteLocation();
        if (rl != null && !rl.getDepartureTime().equals(RouteLocation.NONE)) {
            // need to forward any changes to departure time
            rl.removePropertyChangeListener(this);
            rl.addPropertyChangeListener(this);
            return rl.getFormatedDepartureTime();
        }
        return (parseTime(getDepartTimeMinutes()));
    }

    /**
     * Get train's departure time in minutes from midnight for sorting
     *
     * @return int hh*60+mm
     */
    public int getDepartTimeMinutes() {
        int hour = Integer.parseInt(getDepartureTimeHour());
        int minute = Integer.parseInt(getDepartureTimeMinute());
        return (hour * 60) + minute;
    }

    public void setDepartureTime(String hour, String minute) {
        String old = _departureTime;
        int h = Integer.parseInt(hour);
        if (h < 10) {
            hour = "0" + h;
        }
        int m = Integer.parseInt(minute);
        if (m < 10) {
            minute = "0" + m;
        }
        String time = hour + ":" + minute;
        _departureTime = time;
        if (!old.equals(time)) {
            setDirtyAndFirePropertyChange(DEPARTURETIME_CHANGED_PROPERTY, old, _departureTime);
            setModified(true);
        }
    }

    public String getDepartureTimeHour() {
        String[] time = getDepartureTime().split(":");
        return time[0];
    }

    public String getDepartureTimeMinute() {
        String[] time = getDepartureTime().split(":");
        return time[1];
    }

    public static final String ALREADY_SERVICED = "-1"; // NOI18N

    /**
     * Gets the expected time when this train will arrive at the location rl.
     * Expected arrival time is based on the number of car pick up and set outs for
     * this train. TODO Doesn't provide expected arrival time if train is in route,
     * instead provides relative time. If train is at or has passed the location
     * return -1.
     *
     * @param routeLocation The RouteLocation.
     * @return expected arrival time in minutes (append AM or PM if 12 hour format)
     */
    public String getExpectedArrivalTime(RouteLocation routeLocation) {
        int minutes = getExpectedTravelTimeInMinutes(routeLocation);
        if (minutes == -1) {
            return ALREADY_SERVICED;
        }
        log.debug("Expected arrival time for train ({}) at ({}), {} minutes", getName(), routeLocation.getName(),
                minutes);
        // TODO use fast clock to get current time vs departure time
        // for now use relative
        return parseTime(minutes);
    }

    public String getExpectedDepartureTime(RouteLocation routeLocation) {
        int minutes = getExpectedTravelTimeInMinutes(routeLocation);
        if (minutes == -1) {
            return ALREADY_SERVICED;
        }
        // figure out the work at this location, note that there can be
        // consecutive locations with the same name
        if (getRoute() != null) {
            boolean foundRouteLocation = false;
            for (RouteLocation rl : getRoute().getLocationsBySequenceList()) {
                if (rl == routeLocation) {
                    foundRouteLocation = true;
                }
                if (foundRouteLocation) {
                    if (rl.getSplitName()
                            .equals(routeLocation.getSplitName())) {
                        minutes = minutes + getWorkTimeAtLocation(rl);
                    } else {
                        break; // done
                    }
                }
            }
        }
        log.debug("Expected departure time {} for train ({}) at ({})", minutes, getName(), routeLocation.getName());
        return parseTime(minutes);
    }

    public int getWorkTimeAtLocation(RouteLocation routeLocation) {
        int minutes = 0;
        // departure?
        if (routeLocation == getTrainDepartsRouteLocation()) {
            return minutes;
        }
        // add any work at this location
        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
            if (rs.getRouteLocation() == routeLocation && !rs.getTrackName().equals(RollingStock.NONE)) {
                minutes += Setup.getSwitchTime();
            }
            if (rs.getRouteDestination() == routeLocation) {
                minutes += Setup.getSwitchTime();
            }
        }
        return minutes;
    }

    public int getExpectedTravelTimeInMinutes(RouteLocation routeLocation) {
        int minutes = 0;
        if (!isTrainEnRoute()) {
            minutes += Integer.parseInt(getDepartureTimeMinute());
            minutes += 60 * Integer.parseInt(getDepartureTimeHour());
        } else {
            minutes = -1; // -1 means train has already served the location
        }
        // boolean trainAt = false;
        boolean trainLocFound = false;
        if (getRoute() != null) {
            List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
            for (int i = 0; i < routeList.size(); i++) {
                RouteLocation rl = routeList.get(i);
                if (rl == routeLocation) {
                    break; // done
                }
                // start recording time after finding where the train is
                if (!trainLocFound && isTrainEnRoute()) {
                    if (rl == getCurrentRouteLocation()) {
                        trainLocFound = true;
                        // add travel time
                        minutes = Setup.getTravelTime();
                    }
                    continue;
                }
                // is there a departure time from this location?
                if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
                    String dt = rl.getDepartureTime();
                    log.debug("Location {} departure time {}", rl.getName(), dt);
                    String[] time = dt.split(":");
                    minutes = 60 * Integer.parseInt(time[0]) + Integer.parseInt(time[1]);
                    // log.debug("New minutes: "+minutes);
                }
                // add wait time
                minutes += rl.getWait();
                // add travel time if new location
                RouteLocation next = routeList.get(i + 1);
                if (next != null &&
                        !rl.getSplitName().equals(next.getSplitName())) {
                    minutes += Setup.getTravelTime();
                }
                // don't count work if there's a departure time
                if (i == 0 || !rl.getDepartureTime().equals(RouteLocation.NONE)) {
                    continue;
                }
                // now add the work at the location
                minutes = minutes + getWorkTimeAtLocation(rl);
            }
        }
        return minutes;
    }

    /**
     * Returns time in hour:minute format
     *
     * @param minutes number of minutes from midnight
     * @return hour:minute (optionally AM:PM format)
     */
    private String parseTime(int minutes) {
        int hours = 0;
        int days = 0;

        if (minutes >= 60) {
            int h = minutes / 60;
            minutes = minutes - h * 60;
            hours += h;
        }

        String d = "";
        if (hours >= 24) {
            int nd = hours / 24;
            hours = hours - nd * 24;
            days += nd;
            d = Integer.toString(days) + ":";
        }

        // AM_PM field
        String am_pm = "";
        if (Setup.is12hrFormatEnabled()) {
            am_pm = " " + Bundle.getMessage("AM");
            if (hours >= 12) {
                hours = hours - 12;
                am_pm = " " + Bundle.getMessage("PM");
            }
            if (hours == 0) {
                hours = 12;
            }
        }

        String h = Integer.toString(hours);
        if (hours < 10) {
            h = "0" + h;
        }
        if (minutes < 10) {
            return d + h + ":0" + minutes + am_pm; // NOI18N
        }
        return d + h + ":" + minutes + am_pm;
    }

    /**
     * Set train requirements. If NO_CABOOSE_OR_FRED, then train doesn't require a
     * caboose or car with FRED.
     *
     * @param requires NO_CABOOSE_OR_FRED, CABOOSE, FRED
     */
    public void setRequirements(int requires) {
        int old = _requires;
        _requires = requires;
        if (old != requires) {
            setDirtyAndFirePropertyChange(TRAIN_REQUIREMENTS_CHANGED_PROPERTY, Integer.toString(old),
                    Integer.toString(requires));
        }
    }

    /**
     * Get a train's requirements with regards to the last car in the train.
     *
     * @return NONE CABOOSE FRED
     */
    public int getRequirements() {
        return _requires;
    }

    public boolean isCabooseNeeded() {
        return (getRequirements() & CABOOSE) == CABOOSE;
    }

    public boolean isFredNeeded() {
        return (getRequirements() & FRED) == FRED;
    }

    public void setRoute(Route route) {
        Route old = _route;
        String oldRoute = NONE;
        String newRoute = NONE;
        if (old != null) {
            old.removePropertyChangeListener(this);
            oldRoute = old.toString();
        }
        if (route != null) {
            route.addPropertyChangeListener(this);
            newRoute = route.toString();
        }
        _route = route;
        _skipLocationsList.clear();
        if (old == null || !old.equals(route)) {
            setDirtyAndFirePropertyChange(TRAIN_ROUTE_CHANGED_PROPERTY, oldRoute, newRoute);
        }
    }

    /**
     * Gets the train's route
     *
     * @return train's route
     */
    public Route getRoute() {
        return _route;
    }

    /**
     * Get's the train's route name.
     *
     * @return Train's route name.
     */
    public String getTrainRouteName() {
        if (getRoute() == null) {
            return NONE;
        }
        return getRoute().getName();
    }

    /**
     * Get the train's departure location's name
     *
     * @return train's departure location's name
     */
    public String getTrainDepartsName() {
        if (getTrainDepartsRouteLocation() != null) {
            return getTrainDepartsRouteLocation().getName();
        }
        return NONE;
    }

    public RouteLocation getTrainDepartsRouteLocation() {
        if (getRoute() == null) {
            return null;
        }
        return getRoute().getDepartsRouteLocation();
    }

    public String getTrainDepartsDirection() {
        String direction = NONE;
        if (getTrainDepartsRouteLocation() != null) {
            direction = getTrainDepartsRouteLocation().getTrainDirectionString();
        }
        return direction;
    }

    /**
     * Get train's final location's name
     *
     * @return train's final location's name
     */
    public String getTrainTerminatesName() {
        if (getTrainTerminatesRouteLocation() != null) {
            return getTrainTerminatesRouteLocation().getName();
        }
        return NONE;
    }

    public RouteLocation getTrainTerminatesRouteLocation() {
        if (getRoute() == null) {
            return null;
        }
        return getRoute().getTerminatesRouteLocation();
    }

    /**
     * Returns the order the train should be blocked.
     *
     * @return routeLocations for this train.
     */
    public List<RouteLocation> getTrainBlockingOrder() {
        if (getRoute() == null) {
            return null;
        }
        return getRoute().getBlockingOrder();
    }

    /**
     * Set train's current route location
     *
     * @param location The current RouteLocation.
     */
    protected void setCurrentLocation(RouteLocation location) {
        RouteLocation old = _current;
        _current = location;
        if ((old != null && !old.equals(location)) || (old == null && location != null)) {
            setDirtyAndFirePropertyChange("current", old, location); // NOI18N
        }
    }

    /**
     * Get train's current location name
     *
     * @return Train's current route location name
     */
    public String getCurrentLocationName() {
        if (getCurrentRouteLocation() == null) {
            return NONE;
        }
        return getCurrentRouteLocation().getName();
    }

    /**
     * Get train's current route location
     *
     * @return Train's current route location
     */
    public RouteLocation getCurrentRouteLocation() {
        if (getRoute() == null) {
            return null;
        }
        if (_current == null) {
            return null;
        }
        // this will verify that the current location still exists
        return getRoute().getLocationById(_current.getId());
    }

    /**
     * Get the train's next location name
     *
     * @return Train's next route location name
     */
    public String getNextLocationName() {
        return getNextLocationName(1);
    }

    /**
     * Get a location name in a train's route from the current train's location. A
     * number of "1" means get the next location name in a train's route.
     *
     * @param number The stop number, must be greater than 0
     * @return Name of the location that is the number of stops away from the
     *         train's current location.
     */
    public String getNextLocationName(int number) {
        RouteLocation rl = getCurrentRouteLocation();
        while (number-- > 0) {
            rl = getNextRouteLocation(rl);
            if (rl == null) {
                return NONE;
            }
        }
        return rl.getName();
    }

    public RouteLocation getNextRouteLocation(RouteLocation currentRouteLocation) {
        if (getRoute() == null) {
            return null;
        }
        List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
        for (int i = 0; i < routeList.size(); i++) {
            RouteLocation rl = routeList.get(i);
            if (rl == currentRouteLocation) {
                i++;
                if (i < routeList.size()) {
                    return routeList.get(i);
                }
                break;
            }
        }
        return null; // At end of route
    }

    public void setDepartureTrack(Track track) {
        Track old = _departureTrack;
        _departureTrack = track;
        if (old != track) {
            setDirtyAndFirePropertyChange("DepartureTrackChanged", old, track); // NOI18N
        }
    }

    public Track getDepartureTrack() {
        return _departureTrack;
    }
    
    public boolean isDepartingStaging() {
        return getDepartureTrack() != null;
    }

    public void setTerminationTrack(Track track) {
        Track old = _terminationTrack;
        _terminationTrack = track;
        if (old != track) {
            setDirtyAndFirePropertyChange("TerminationTrackChanged", old, track); // NOI18N
        }
    }

    public Track getTerminationTrack() {
        return _terminationTrack;
    }

    /**
     * Set the train's machine readable status. Calls update train table row color.
     *
     * @param code machine readable
     */
    public void setStatusCode(int code) {
        String oldStatus = getStatus();
        int oldCode = getStatusCode();
        _statusCode = code;
        // always fire property change for train en route
        if (oldCode != getStatusCode() || code == CODE_TRAIN_EN_ROUTE) {
            setDirtyAndFirePropertyChange(STATUS_CHANGED_PROPERTY, oldStatus, getStatus());
        }
        updateTrainTableRowColor();
    }

    public void updateTrainTableRowColor() {
        if (!InstanceManager.getDefault(TrainManager.class).isRowColorManual()) {
            switch (getStatusCode()) {
                case CODE_TRAIN_RESET:
                    String color = getTableRowColorNameReset();
                    if (color.equals(NONE)) {
                        color = InstanceManager.getDefault(TrainManager.class).getRowColorNameForReset();
                    }
                    setTableRowColorName(color);
                    break;
                case CODE_BUILT:
                case CODE_PARTIAL_BUILT:
                    setTableRowColorName(InstanceManager.getDefault(TrainManager.class).getRowColorNameForBuilt());
                    break;
                case CODE_BUILD_FAILED:
                    setTableRowColorName(
                            InstanceManager.getDefault(TrainManager.class).getRowColorNameForBuildFailed());
                    break;
                case CODE_TRAIN_EN_ROUTE:
                    setTableRowColorName(
                            InstanceManager.getDefault(TrainManager.class).getRowColorNameForTrainEnRoute());
                    break;
                case CODE_TERMINATED:
                    setTableRowColorName(InstanceManager.getDefault(TrainManager.class).getRowColorNameForTerminated());
                    break;
                default: // all other cases do nothing
                    break;
            }
        }
    }

    /**
     * Get train's status in the default locale.
     *
     * @return Human-readable status
     */
    public String getStatus() {
        return this.getStatus(Locale.getDefault());
    }

    /**
     * Get train's status in the specified locale.
     *
     * @param locale The Locale.
     * @return Human-readable status
     */
    public String getStatus(Locale locale) {
        return this.getStatus(locale, this.getStatusCode());
    }

    /**
     * Get the human-readable status for the requested status code.
     *
     * @param locale The Locale.
     * @param code   requested status
     * @return Human-readable status
     */
    public String getStatus(Locale locale, int code) {
        switch (code) {
            case CODE_RUN_SCRIPTS:
                return RUN_SCRIPTS;
            case CODE_BUILDING:
                return BUILDING;
            case CODE_BUILD_FAILED:
                return BUILD_FAILED;
            case CODE_BUILT:
                return Bundle.getMessage(locale, "StatusBuilt", this.getNumberCarsWorked()); // NOI18N
            case CODE_PARTIAL_BUILT:
                return Bundle.getMessage(locale, "StatusPartialBuilt", this.getNumberCarsWorked(),
                        this.getNumberCarsRequested()); // NOI18N
            case CODE_TERMINATED:
                return Bundle.getMessage(locale, "StatusTerminated", this.getTerminationDate()); // NOI18N
            case CODE_TRAIN_EN_ROUTE:
                return Bundle.getMessage(locale, "StatusEnRoute", this.getNumberCarsInTrain(), this.getTrainLength(),
                        Setup.getLengthUnit().toLowerCase(), this.getTrainWeight()); // NOI18N
            case CODE_TRAIN_RESET:
                return TRAIN_RESET;
            case CODE_MANIFEST_MODIFIED:
                return MANIFEST_MODIFIED;
            case CODE_UNKNOWN:
            default:
                return UNKNOWN;
        }
    }

    public String getMRStatus() {
        switch (getStatusCode()) {
            case CODE_PARTIAL_BUILT:
                return getStatusCode() + "||" + this.getNumberCarsRequested(); // NOI18N
            case CODE_TERMINATED:
                return getStatusCode() + "||" + this.getTerminationDate(); // NOI18N
            default:
                return Integer.toString(getStatusCode());
        }
    }

    public int getStatusCode() {
        return _statusCode;
    }

    protected void setOldStatusCode(int code) {
        _oldStatusCode = code;
    }

    protected int getOldStatusCode() {
        return _oldStatusCode;
    }

    /**
     * Used to determine if train has departed the first location in the train's
     * route
     *
     * @return true if train has departed
     */
    public boolean isTrainEnRoute() {
        return !getCurrentLocationName().equals(NONE) && getTrainDepartsRouteLocation() != getCurrentRouteLocation();
    }

    /**
     * Used to determine if train is a local switcher serving one location. Note the
     * train can have more than location in its route, but all location names must
     * be "same". See TrainCommon.splitString(String name) for the definition of the
     * "same" name.
     *
     * @return true if local switcher
     */
    public boolean isLocalSwitcher() {
        String departureName = TrainCommon.splitString(getTrainDepartsName());
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                if (!departureName.equals(rl.getSplitName())) {
                    return false; // not a local switcher
                }
            }
        }
        return true;
    }
    
    public boolean isTurn() {
        return !isLocalSwitcher() &&
                TrainCommon.splitString(getTrainDepartsName())
                        .equals(TrainCommon.splitString(getTrainTerminatesName()));
    }

    /**
     * Used to determine if train is carrying only passenger cars.
     *
     * @return true if only passenger cars have been assigned to this train.
     */
    public boolean isOnlyPassengerCars() {
        for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
            if (!car.isPassenger()) {
                return false;
            }
        }
        return true;
    }

    List<String> _skipLocationsList = new ArrayList<>();

    protected String[] getTrainSkipsLocations() {
        String[] locationIds = new String[_skipLocationsList.size()];
        for (int i = 0; i < _skipLocationsList.size(); i++) {
            locationIds[i] = _skipLocationsList.get(i);
        }
        return locationIds;
    }

    protected void setTrainSkipsLocations(String[] locationIds) {
        if (locationIds.length > 0) {
            Arrays.sort(locationIds);
            for (String id : locationIds) {
                _skipLocationsList.add(id);
            }
        }
    }

    /**
     * Train will skip the RouteLocation
     *
     * @param routelocationId RouteLocation Id
     */
    public void addTrainSkipsLocation(String routelocationId) {
        // insert at start of _skipLocationsList, sort later
        if (!_skipLocationsList.contains(routelocationId)) {
            _skipLocationsList.add(0, routelocationId);
            log.debug("train does not stop at {}", routelocationId);
            setDirtyAndFirePropertyChange(STOPS_CHANGED_PROPERTY, _skipLocationsList.size() - 1,
                    _skipLocationsList.size());
        }
    }

    public void deleteTrainSkipsLocation(String locationId) {
        _skipLocationsList.remove(locationId);
        log.debug("train will stop at {}", locationId);
        setDirtyAndFirePropertyChange(STOPS_CHANGED_PROPERTY, _skipLocationsList.size() + 1, _skipLocationsList.size());
    }

    /**
     * Determines if this train skips a location (doesn't service the location).
     *
     * @param locationId The route location id.
     * @return true if the train will not service the location.
     */
    public boolean isLocationSkipped(String locationId) {
        return _skipLocationsList.contains(locationId);
    }

    List<String> _typeList = new ArrayList<>();

    /**
     * Get's the type names of rolling stock this train will service
     *
     * @return The type names for cars and or engines
     */
    protected String[] getTypeNames() {
        return _typeList.toArray(new String[0]);
    }

    public String[] getCarTypeNames() {
        List<String> list = new ArrayList<>();
        for (String type : _typeList) {
            if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
                list.add(type);
            }
        }
        return list.toArray(new String[0]);
    }

    public String[] getLocoTypeNames() {
        List<String> list = new ArrayList<>();
        for (String type : _typeList) {
            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
                list.add(type);
            }
        }
        return list.toArray(new String[0]);
    }

    /**
     * Set the type of cars or engines this train will service, see types in Cars
     * and Engines.
     *
     * @param types The type names for cars and or engines
     */
    protected void setTypeNames(String[] types) {
        if (types.length > 0) {
            Arrays.sort(types);
            for (String type : types) {
                _typeList.add(type);
            }
        }
    }

    /**
     * Add a car or engine type name that this train will service.
     *
     * @param type The new type name to service.
     */
    public void addTypeName(String type) {
        // insert at start of list, sort later
        if (type == null || _typeList.contains(type)) {
            return;
        }
        _typeList.add(0, type);
        log.debug("Train ({}) add car type ({})", getName(), type);
        setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() - 1, _typeList.size());
    }

    public void deleteTypeName(String type) {
        if (_typeList.remove(type)) {
            log.debug("Train ({}) delete car type ({})", getName(), type);
            setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() + 1, _typeList.size());
        }
    }

    /**
     * Returns true if this train will service the type of car or engine.
     *
     * @param type The car or engine type name.
     * @return true if this train will service the particular type.
     */
    public boolean isTypeNameAccepted(String type) {
        return _typeList.contains(type);
    }

    protected void replaceType(String oldType, String newType) {
        if (isTypeNameAccepted(oldType)) {
            deleteTypeName(oldType);
            addTypeName(newType);
            // adjust loads with type in them
            for (String load : getLoadNames()) {
                String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
                if (splitLoad.length > 1) {
                    if (splitLoad[0].equals(oldType)) {
                        deleteLoadName(load);
                        if (newType != null) {
                            load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
                            addLoadName(load);
                        }
                    }
                }
            }
        }
    }

    /**
     * Get how this train deals with car road names.
     *
     * @return ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
     */
    public String getCarRoadOption() {
        return _carRoadOption;
    }

    /**
     * Set how this train deals with car road names.
     *
     * @param option ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
     */
    public void setCarRoadOption(String option) {
        String old = _carRoadOption;
        _carRoadOption = option;
        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
    }

    List<String> _carRoadList = new ArrayList<>();

    protected void setCarRoadNames(String[] roads) {
        setRoadNames(roads, _carRoadList);
    }

    /**
     * Provides a list of car road names that the train will either service or exclude.
     * See setCarRoadOption
     *
     * @return Array of sorted road names as Strings
     */
    public String[] getCarRoadNames() {
        String[] roads = _carRoadList.toArray(new String[0]);
        if (_carRoadList.size() > 0) {
            Arrays.sort(roads);
        }
        return roads;
    }

    /**
     * Add a car road name that the train will either service or exclude. See
     * setCarRoadOption
     *
     * @param road The string road name.
     * @return true if road name was added, false if road name wasn't in the list.
     */
    public boolean addCarRoadName(String road) {
        if (_carRoadList.contains(road)) {
            return false;
        }
        _carRoadList.add(road);
        log.debug("train ({}) add car road {}", getName(), road);
        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _carRoadList.size() - 1, _carRoadList.size());
        return true;
    }

    /**
     * Delete a car road name that the train will either service or exclude. See
     * setRoadOption
     *
     * @param road The string road name to delete.
     * @return true if road name was removed, false if road name wasn't in the list.
     */
    public boolean deleteCarRoadName(String road) {
        if (_carRoadList.remove(road)) {
            log.debug("train ({}) delete car road {}", getName(), road);
            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _carRoadList.size() + 1, _carRoadList.size());
            return true;
        }
        return false;
    }

    /**
     * Determine if train will service a specific road name for a car.
     *
     * @param road the road name to check.
     * @return true if train will service this road name.
     */
    public boolean isCarRoadNameAccepted(String road) {
        if (_carRoadOption.equals(ALL_ROADS)) {
            return true;
        }
        if (_carRoadOption.equals(INCLUDE_ROADS)) {
            return _carRoadList.contains(road);
        }
        // exclude!
        return !_carRoadList.contains(road);
    }
    
    /**
     * Get how this train deals with locomotive road names.
     *
     * @return ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
     */
    public String getLocoRoadOption() {
        return _locoRoadOption;
    }

    /**
     * Set how this train deals with locomotive road names.
     *
     * @param option ALL_ROADS INCLUDE_ROADS EXCLUDE_ROADS
     */
    public void setLocoRoadOption(String option) {
        String old = _locoRoadOption;
        _locoRoadOption = option;
        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
    }

    List<String> _locoRoadList = new ArrayList<>();

    protected void setLocoRoadNames(String[] roads) {
        setRoadNames(roads, _locoRoadList);
    }
    
    private void setRoadNames(String[] roads, List<String> list) {
        if (roads.length > 0) {
            Arrays.sort(roads);
            for (String road : roads) {
                if (!road.isEmpty()) {
                    list.add(road);
                }
            }
        }
    }

    /**
     * Provides a list of engine road names that the train will either service or exclude.
     * See setLocoRoadOption
     *
     * @return Array of sorted road names as Strings
     */
    public String[] getLocoRoadNames() {
        String[] roads = _locoRoadList.toArray(new String[0]);
        if (_locoRoadList.size() > 0) {
            Arrays.sort(roads);
        }
        return roads;
    }

    /**
     * Add a engine road name that the train will either service or exclude. See
     * setLocoRoadOption
     *
     * @param road The string road name.
     * @return true if road name was added, false if road name wasn't in the list.
     */
    public boolean addLocoRoadName(String road) {
        if (_locoRoadList.contains(road)) {
            return false;
        }
        _locoRoadList.add(road);
        log.debug("train ({}) add engine road {}", getName(), road);
        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _locoRoadList.size() - 1, _locoRoadList.size());
        return true;
    }

    /**
     * Delete a engine road name that the train will either service or exclude. See
     * setLocoRoadOption
     *
     * @param road The string road name to delete.
     * @return true if road name was removed, false if road name wasn't in the list.
     */
    public boolean deleteLocoRoadName(String road) {
        if (_locoRoadList.remove(road)) {
            log.debug("train ({}) delete engine road {}", getName(), road);
            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _locoRoadList.size() + 1, _locoRoadList.size());
            return true;
        }
        return false;
    }

    /**
     * Determine if train will service a specific road name for an engine.
     *
     * @param road the road name to check.
     * @return true if train will service this road name.
     */
    public boolean isLocoRoadNameAccepted(String road) {
        if (_locoRoadOption.equals(ALL_ROADS)) {
            return true;
        }
        if (_locoRoadOption.equals(INCLUDE_ROADS)) {
            return _locoRoadList.contains(road);
        }
        // exclude!
        return !_locoRoadList.contains(road);
    }

    protected void replaceRoad(String oldRoad, String newRoad) {
        if (newRoad != null) {
            if (deleteCarRoadName(oldRoad)) {
                addCarRoadName(newRoad);
            }
            if (deleteLocoRoadName(oldRoad)) {
                addLocoRoadName(newRoad);
            }
            if (getEngineRoad().equals(oldRoad)) {
                setEngineRoad(newRoad);
            }
            if (getCabooseRoad().equals(oldRoad)) {
                setCabooseRoad(newRoad);
            }
            if (getSecondLegEngineRoad().equals(oldRoad)) {
                setSecondLegEngineRoad(newRoad);
            }
            if (getSecondLegCabooseRoad().equals(oldRoad)) {
                setSecondLegCabooseRoad(newRoad);
            }
            if (getThirdLegEngineRoad().equals(oldRoad)) {
                setThirdLegEngineRoad(newRoad);
            }
            if (getThirdLegCabooseRoad().equals(oldRoad)) {
                setThirdLegCabooseRoad(newRoad);
            }
        }
    }

    /**
     * Gets the car load option for this train.
     *
     * @return ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
     */
    public String getLoadOption() {
        return _loadOption;
    }

    /**
     * Set how this train deals with car loads
     *
     * @param option ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
     */
    public void setLoadOption(String option) {
        String old = _loadOption;
        _loadOption = option;
        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, old, option);
    }

    List<String> _loadList = new ArrayList<>();

    protected void setLoadNames(String[] loads) {
        if (loads.length > 0) {
            Arrays.sort(loads);
            for (String load : loads) {
                if (!load.isEmpty()) {
                    _loadList.add(load);
                }
            }
        }
    }

    /**
     * Provides a list of loads that the train will either service or exclude. See
     * setLoadOption
     *
     * @return Array of load names as Strings
     */
    public String[] getLoadNames() {
        String[] loads = _loadList.toArray(new String[0]);
        if (_loadList.size() > 0) {
            Arrays.sort(loads);
        }
        return loads;
    }

    /**
     * Add a load that the train will either service or exclude. See setLoadOption
     *
     * @param load The string load name.
     * @return true if load name was added, false if load name wasn't in the list.
     */
    public boolean addLoadName(String load) {
        if (_loadList.contains(load)) {
            return false;
        }
        _loadList.add(load);
        log.debug("train ({}) add car load {}", getName(), load);
        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() - 1, _loadList.size());
        return true;
    }

    /**
     * Delete a load name that the train will either service or exclude. See
     * setLoadOption
     *
     * @param load The string load name.
     * @return true if load name was removed, false if load name wasn't in the list.
     */
    public boolean deleteLoadName(String load) {
        if (_loadList.remove(load)) {
            log.debug("train ({}) delete car load {}", getName(), load);
            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() + 1, _loadList.size());
            return true;
        }
        return false;
    }

    /**
     * Determine if train will service a specific load name.
     *
     * @param load the load name to check.
     * @return true if train will service this load.
     */
    public boolean isLoadNameAccepted(String load) {
        if (_loadOption.equals(ALL_LOADS)) {
            return true;
        }
        if (_loadOption.equals(INCLUDE_LOADS)) {
            return _loadList.contains(load);
        }
        // exclude!
        return !_loadList.contains(load);
    }

    /**
     * Determine if train will service a specific load and car type.
     *
     * @param load the load name to check.
     * @param type the type of car used to carry the load.
     * @return true if train will service this load.
     */
    public boolean isLoadNameAccepted(String load, String type) {
        if (_loadOption.equals(ALL_LOADS)) {
            return true;
        }
        if (_loadOption.equals(INCLUDE_LOADS)) {
            return _loadList.contains(load) || _loadList.contains(type + CarLoad.SPLIT_CHAR + load);
        }
        // exclude!
        return !_loadList.contains(load) && !_loadList.contains(type + CarLoad.SPLIT_CHAR + load);
    }

    public String getOwnerOption() {
        return _ownerOption;
    }

    /**
     * Set how this train deals with car owner names
     *
     * @param option ALL_OWNERS INCLUDE_OWNERS EXCLUDE_OWNERS
     */
    public void setOwnerOption(String option) {
        String old = _ownerOption;
        _ownerOption = option;
        setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, old, option);
    }

    List<String> _ownerList = new ArrayList<>();

    protected void setOwnerNames(String[] owners) {
        if (owners.length > 0) {
            Arrays.sort(owners);
            for (String owner : owners) {
                if (!owner.isEmpty()) {
                    _ownerList.add(owner);
                }
            }
        }
    }

    /**
     * Provides a list of owner names that the train will either service or exclude.
     * See setOwnerOption
     *
     * @return Array of owner names as Strings
     */
    public String[] getOwnerNames() {
        String[] owners = _ownerList.toArray(new String[0]);
        if (_ownerList.size() > 0) {
            Arrays.sort(owners);
        }
        return owners;
    }

    /**
     * Add a owner name that the train will either service or exclude. See
     * setOwnerOption
     *
     * @param owner The string representing the owner's name.
     * @return true if owner name was added, false if owner name wasn't in the list.
     */
    public boolean addOwnerName(String owner) {
        if (_ownerList.contains(owner)) {
            return false;
        }
        _ownerList.add(owner);
        log.debug("train ({}) add car owner {}", getName(), owner);
        setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, _ownerList.size() - 1, _ownerList.size());
        return true;
    }

    /**
     * Delete a owner name that the train will either service or exclude. See
     * setOwnerOption
     *
     * @param owner The string representing the owner's name.
     * @return true if owner name was removed, false if owner name wasn't in the
     *         list.
     */
    public boolean deleteOwnerName(String owner) {
        if (_ownerList.remove(owner)) {
            log.debug("train ({}) delete car owner {}", getName(), owner);
            setDirtyAndFirePropertyChange(OWNERS_CHANGED_PROPERTY, _ownerList.size() + 1, _ownerList.size());
            return true;
        }
        return false;
    }

    /**
     * Determine if train will service a specific owner name.
     *
     * @param owner the owner name to check.
     * @return true if train will service this owner name.
     */
    public boolean isOwnerNameAccepted(String owner) {
        if (_ownerOption.equals(ALL_OWNERS)) {
            return true;
        }
        if (_ownerOption.equals(INCLUDE_OWNERS)) {
            return _ownerList.contains(owner);
        }
        // exclude!
        return !_ownerList.contains(owner);
    }

    protected void replaceOwner(String oldName, String newName) {
        if (deleteOwnerName(oldName)) {
            addOwnerName(newName);
        }
    }

    /**
     * Only rolling stock built in or after this year will be used.
     *
     * @param year A string representing a year.
     */
    public void setBuiltStartYear(String year) {
        String old = _builtStartYear;
        _builtStartYear = year;
        if (!old.equals(year)) {
            setDirtyAndFirePropertyChange(BUILT_YEAR_CHANGED_PROPERTY, old, year);
        }
    }

    public String getBuiltStartYear() {
        return _builtStartYear;
    }

    /**
     * Only rolling stock built in or before this year will be used.
     *
     * @param year A string representing a year.
     */
    public void setBuiltEndYear(String year) {
        String old = _builtEndYear;
        _builtEndYear = year;
        if (!old.equals(year)) {
            setDirtyAndFirePropertyChange(BUILT_YEAR_CHANGED_PROPERTY, old, year);
        }
    }

    public String getBuiltEndYear() {
        return _builtEndYear;
    }

    /**
     * Determine if train will service rolling stock by built date.
     *
     * @param date A string representing the built date for a car or engine.
     * @return true is built date is in the acceptable range.
     */
    public boolean isBuiltDateAccepted(String date) {
        if (getBuiltStartYear().equals(NONE) && getBuiltEndYear().equals(NONE)) {
            return true; // range dates not defined
        }
        int startYear = 0; // default start year;
        int endYear = 99999; // default end year;
        int builtYear = -1900;
        if (!getBuiltStartYear().equals(NONE)) {
            try {
                startYear = Integer.parseInt(getBuiltStartYear());
            } catch (NumberFormatException e) {
                log.debug("Train ({}) built start date not initialized, start: {}", getName(), getBuiltStartYear());
            }
        }
        if (!getBuiltEndYear().equals(NONE)) {
            try {
                endYear = Integer.parseInt(getBuiltEndYear());
            } catch (NumberFormatException e) {
                log.debug("Train ({}) built end date not initialized, end: {}", getName(), getBuiltEndYear());
            }
        }
        try {
            builtYear = Integer.parseInt(RollingStockManager.convertBuildDate(date));
        } catch (NumberFormatException e) {
            log.debug("Unable to parse car built date {}", date);
        }
        if (startYear < builtYear && builtYear < endYear) {
            return true;
        }
        return false;
    }

    private final boolean debugFlag = false;

    /**
     * Determines if this train will service this car. Note this code doesn't check
     * the location or tracks that needs to be done separately. See Router.java.
     *
     * @param car The car to be tested.
     * @return true if this train can service the car.
     */
    public boolean isServiceable(Car car) {
        return isServiceable(null, car);
    }

    /**
     * Note that this code was written after TrainBuilder. It does pretty much the
     * same as TrainBuilder but with much fewer build report messages.
     *
     * @param buildReport PrintWriter
     * @param car         the car to be tested
     * @return true if this train can service the car.
     */
    public boolean isServiceable(PrintWriter buildReport, Car car) {
        setServiceStatus(NONE);
        // check to see if train can carry car
        if (!isTypeNameAccepted(car.getTypeName())) {
            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarType",
                    getName(), car.toString(), car.getTypeName()));
            return false;
        }
        if (!isLoadNameAccepted(car.getLoadName(), car.getTypeName())) {
            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarLoad",
                    getName(), car.toString(), car.getTypeName(), car.getLoadName()));
            return false;
        }
        if (!isBuiltDateAccepted(car.getBuilt()) ||
                !isOwnerNameAccepted(car.getOwnerName()) ||
                !isCarRoadNameAccepted(car.getRoadName())) {
            addLine(buildReport, Bundle.getMessage("trainCanNotServiceCar",
                    getName(), car.toString()));
            return false;
        }

        Route route = getRoute();
        if (route == null) {
            return false;
        }

        if (car.getLocation() == null || car.getTrack() == null) {
            return false;
        }

        // determine if the car's location and destination is serviced by this
        // train
        if (route.getLastLocationByName(car.getLocationName()) == null) {
            addLine(buildReport, Bundle.getMessage("trainNotThisLocation",
                    getName(), car.getLocationName()));
            return false;
        }
        if (car.getDestination() != null && route.getLastLocationByName(car.getDestinationName()) == null) {
            addLine(buildReport, Bundle.getMessage("trainNotThisLocation",
                    getName(), car.getDestinationName()));
            return false;
        }
        // now find the car in the train's route
        List<RouteLocation> rLocations = route.getLocationsBySequenceList();
        for (RouteLocation rLoc : rLocations) {
            if (rLoc.getName().equals(car.getLocationName()) &&
                    rLoc.isPickUpAllowed() &&
                    rLoc.getMaxCarMoves() > 0 &&
                    !isLocationSkipped(rLoc.getId()) &&
                    ((car.getLocation().getTrainDirections() & rLoc.getTrainDirection()) != 0 || isLocalSwitcher())) {

                if (((car.getTrack().getTrainDirections() & rLoc.getTrainDirection()) == 0 && !isLocalSwitcher()) ||
                        !car.getTrack().isPickupTrainAccepted(this)) {
                    addLine(buildReport,
                            Bundle.getMessage("trainCanNotServiceCarFrom",
                                    getName(), car.toString(), car.getLocationName(), car.getTrackName(),
                                            rLoc.getId()));
                    continue;
                }
                if (debugFlag) {
                    log.debug("Car ({}) can be picked up by train ({}) location ({}, {}) destination ({}, {})",
                            car.toString(), getName(), car.getLocationName(), car.getTrackName(),
                            car.getDestinationName(), car.getDestinationTrackName());
                }
                addLine(buildReport, Bundle.getMessage("trainCanPickUpCar",
                        getName(), car.toString(), car.getLocationName(), car.getTrackName(), rLoc.getId()));
                if (car.getDestination() == null) {
                    if (debugFlag) {
                        log.debug("Car ({}) does not have a destination", car.toString());
                    }
                    return true;
                }
                // now check car's destination
                return isServiceableDestination(buildReport, car, rLoc, rLocations);
            } else if (rLoc.getName().equals(car.getLocationName())) {
                addLine(buildReport, Bundle.getMessage("trainCanNotServiceCarFrom",
                        getName(), car.toString(), car.getLocationName(), car.getTrackName(), rLoc.getId()));
            }
        }
        if (debugFlag) {
            log.debug("Train ({}) can't service car ({}) from ({}, {})", getName(), car.toString(),
                    car.getLocationName(), car.getTrackName());
        }
        return false;
    }

    /**
     * Second step in determining if train can service car, check to see if car's
     * destination is serviced by this train's route.
     *
     * @param buildReport add messages if needed to build report
     * @param car         The test car
     * @param rLoc        Where in the train's route the car was found
     * @param rLocations  The ordered routeLocations in this train's route
     * @return true if car's destination can be serviced
     */
    private boolean isServiceableDestination(PrintWriter buildReport, Car car, RouteLocation rLoc,
            List<RouteLocation> rLocations) {
        // need the car's length when building train
        int length = car.getTotalLength();
        // car can be a kernel so get total length
        if (car.getKernel() != null) {
            length = car.getKernel().getTotalLength();
        }
        // now see if the train's route services the car's destination
        for (int k = rLocations.indexOf(rLoc); k < rLocations.size(); k++) {
            RouteLocation rldest = rLocations.get(k);
            if (rldest.getName().equals(car.getDestinationName()) &&
                    rldest.isDropAllowed() &&
                    rldest.getMaxCarMoves() > 0 &&
                    !isLocationSkipped(rldest.getId()) &&
                    ((car.getDestination().getTrainDirections() & rldest.getTrainDirection()) != 0 ||
                            isLocalSwitcher()) &&
                    (!Setup.isCheckCarDestinationEnabled() ||
                            car.getTrack().isDestinationAccepted(car.getDestination()))) {
                // found a destination, now check destination track
                if (car.getDestinationTrack() != null) {
                    if (!isServicableTrack(buildReport, car, rldest, car.getDestinationTrack())) {
                        continue;
                    }
                } else if (rldest.getLocation().isStaging() &&
                        getStatusCode() == CODE_BUILDING &&
                        getTerminationTrack() != null &&
                        getTerminationTrack().getLocation() == rldest.getLocation()) {
                    if (debugFlag) {
                        log.debug("Car ({}) destination is staging, check train ({}) termination track ({})",
                                car.toString(), getName(), getTerminationTrack().getName());
                    }
                    String status = car.checkDestination(getTerminationTrack().getLocation(), getTerminationTrack());
                    if (!status.equals(Track.OKAY)) {
                        addLine(buildReport,
                                Bundle.getMessage("trainCanNotDeliverToStaging",
                                        getName(), car.toString(),
                                                getTerminationTrack().getLocation().getName(),
                                                getTerminationTrack().getName(), status));
                        setServiceStatus(status);
                        continue;
                    }
                } else {
                    if (debugFlag) {
                        log.debug("Find track for car ({}) at destination ({})", car.toString(),
                                car.getDestinationName());
                    }
                    // determine if there's a destination track that is willing to accept this car
                    String status = "";
                    List<Track> tracks = rldest.getLocation().getTracksList();
                    for (Track track : tracks) {
                        if (!isServicableTrack(buildReport, car, rldest, track)) {
                            continue;
                        }
                        // will the track accept this car?
                        status = track.isRollingStockAccepted(car);
                        if (status.equals(Track.OKAY) || status.startsWith(Track.LENGTH)) {
                            if (debugFlag) {
                                log.debug("Found track ({}) for car ({})", track.getName(), car.toString());
                            }
                            break; // found track
                        }
                    }
                    if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
                        if (debugFlag) {
                            log.debug("Destination ({}) can not service car ({}) using train ({}) no track available",
                                    car.getDestinationName(), car.toString(), getName()); // NOI18N
                        }
                        addLine(buildReport, Bundle.getMessage("trainCanNotDeliverNoTracks",
                                getName(), car.toString(), car.getDestinationName(), rldest.getId()));
                        continue;
                    }
                }
                // restriction to only carry cars to terminal?
                // ignore send to terminal if a local move
                if (isSendCarsToTerminalEnabled() &&
                        !car.isLocalMove() &&
                        !car.getSplitLocationName()
                                .equals(TrainCommon.splitString(getTrainDepartsName())) &&
                        !car.getSplitDestinationName()
                                .equals(TrainCommon.splitString(getTrainTerminatesName()))) {
                    if (debugFlag) {
                        log.debug("option send cars to terminal is enabled");
                    }
                    addLine(buildReport,
                            Bundle.getMessage("trainCanNotCarryCarOption",
                                    getName(), car.toString(), car.getLocationName(),
                                            car.getTrackName(), car.getDestinationName(),
                                            car.getDestinationTrackName()));
                    continue;
                }
                // don't allow local move when car is in staging
                if (!isTurn() && car.getTrack().isStaging() &&
                        rldest.getLocation() == car.getLocation()) {
                    log.debug(
                            "Car ({}) at ({}, {}) not allowed to perform local move in staging ({})",
                            car.toString(), car.getLocationName(), car.getTrackName(), rldest.getName());
                    continue;
                }
                // allow car to return to staging?
                if (isAllowReturnToStagingEnabled() &&
                        car.getTrack().isStaging() &&
                        rldest.getLocation() == car.getLocation()) {
                    addLine(buildReport,
                            Bundle.getMessage("trainCanReturnCarToStaging",
                                    getName(), car.toString(), car.getDestinationName(),
                                            car.getDestinationTrackName()));
                    return true;
                }
                // is this a local move?
                if (!isLocalSwitcher() &&
                        !isAllowLocalMovesEnabled() &&
                        !car.isCaboose() &&
                        !car.hasFred() &&
                        !car.isPassenger() &&
                        car.isLocalMove()) {
                    if (debugFlag) {
                        log.debug("Local move not allowed");
                    }
                    addLine(buildReport, Bundle.getMessage("trainCanNotPerformLocalMove",
                            getName(), car.toString(), car.getLocationName()));
                    continue;
                }
                // Can cars travel from origin to terminal?
                if (!isAllowThroughCarsEnabled() &&
                        TrainCommon.splitString(getTrainDepartsName())
                                .equals(rLoc.getSplitName()) &&
                        TrainCommon.splitString(getTrainTerminatesName())
                                .equals(rldest.getSplitName()) &&
                        !TrainCommon.splitString(getTrainDepartsName())
                                .equals(TrainCommon.splitString(getTrainTerminatesName())) &&
                        !isLocalSwitcher() &&
                        !car.isCaboose() &&
                        !car.hasFred() &&
                        !car.isPassenger()) {
                    if (debugFlag) {
                        log.debug("Through car ({}) not allowed", car.toString());
                    }
                    addLine(buildReport, Bundle.getMessage("trainDoesNotCarryOriginTerminal",
                            getName(), car.getLocationName(), car.getDestinationName()));
                    continue;
                }
                // check to see if moves are available
                if (getStatusCode() == CODE_BUILDING && rldest.getMaxCarMoves() - rldest.getCarMoves() <= 0) {
                    setServiceStatus(Bundle.getMessage("trainNoMoves",
                            getName(), getRoute().getName(), rldest.getId(), rldest.getName()));
                    if (debugFlag) {
                        log.debug("No available moves for destination {}", rldest.getName());
                    }
                    addLine(buildReport, getServiceStatus());
                    continue;
                }
                if (debugFlag) {
                    log.debug("Car ({}) can be dropped by train ({}) to ({}, {})", car.toString(), getName(),
                            car.getDestinationName(), car.getDestinationTrackName());
                }
                return true;
            }
            // check to see if train length is okay
            if (getStatusCode() == CODE_BUILDING && rldest.getTrainLength() + length > rldest.getMaxTrainLength()) {
                setServiceStatus(Bundle.getMessage("trainExceedsMaximumLength",
                        getName(), getRoute().getName(), rldest.getId(), rldest.getMaxTrainLength(),
                                Setup.getLengthUnit().toLowerCase(), rldest.getName(), car.toString(),
                                rldest.getTrainLength() + length - rldest.getMaxTrainLength()));
                if (debugFlag) {
                    log.debug("Car ({}) exceeds maximum train length {} when departing ({})", car.toString(),
                            rldest.getMaxTrainLength(), rldest.getName());
                }
                addLine(buildReport, getServiceStatus());
                return false;
            }
        }
        addLine(buildReport, Bundle.getMessage("trainCanNotDeliverToDestination",
                getName(), car.toString(), car.getDestinationName(), car.getDestinationTrackName()));
        return false;
    }

    private boolean isServicableTrack(PrintWriter buildReport, Car car, RouteLocation rldest, Track track) {
        if ((track.getTrainDirections() & rldest.getTrainDirection()) == 0 && !isLocalSwitcher()) {
            addLine(buildReport, Bundle.getMessage("buildCanNotDropRsUsingTrain",
                    car.toString(), rldest.getTrainDirectionString(), track.getName()));
            return false;
        }
        if (!track.isDropTrainAccepted(this)) {
            addLine(buildReport, Bundle.getMessage("buildCanNotDropCarTrain",
                    car.toString(), getName(), track.getTrackTypeName(), track.getLocation().getName(),
                            track.getName()));
            return false;
        }
        return true;
    }

    protected static final String SEVEN = Setup.BUILD_REPORT_VERY_DETAILED;

    private void addLine(PrintWriter buildReport, String string) {
        if (Setup.getRouterBuildReportLevel().equals(SEVEN)) {
            TrainCommon.addLine(buildReport, SEVEN, string);
        }
    }

    protected void setServiceStatus(String status) {
        _serviceStatus = status;
    }

    /**
     * Returns the statusCode of the "isServiceable(Car)" routine. There are two
     * statusCodes that need special consideration when the train is being built,
     * the moves in a train's route and the maximum train length. NOTE: The code
     * using getServiceStatus() currently assumes that if there's a service status
     * that the issue is either route moves or maximum train length.
     *
     * @return The statusCode.
     */
    public String getServiceStatus() {
        return _serviceStatus;
    }

    /**
     * @return The number of cars worked by this train
     */
    public int getNumberCarsWorked() {
        int count = 0;
        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
            if (rs.getRouteLocation() != null) {
                count++;
            }
        }
        return count;
    }

    public void setNumberCarsRequested(int number) {
        _statusCarsRequested = number;
    }

    public int getNumberCarsRequested() {
        return _statusCarsRequested;
    }

    public void setTerminationDate(String date) {
        _statusTerminatedDate = date;
    }

    public String getTerminationDate() {
        return _statusTerminatedDate;
    }

    /**
     * Gets the number of cars in the train at the current location in the train's
     * route.
     *
     * @return The number of cars currently in the train
     */
    public int getNumberCarsInTrain() {
        return getNumberCarsInTrain(getCurrentRouteLocation());
    }

    /**
     * Gets the number of cars in the train when train departs the route location.
     *
     * @param routeLocation The RouteLocation.
     * @return The number of cars in the train departing the route location.
     */
    public int getNumberCarsInTrain(RouteLocation routeLocation) {
        int number = 0;
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
                    if (rs.getRouteLocation() == rl) {
                        number++;
                    }
                    if (rs.getRouteDestination() == rl) {
                        number--;
                    }
                }
                if (rl == routeLocation) {
                    break;
                }
            }
        }
        return number;
    }

    /**
     * Gets the number of empty cars in the train when train departs the route
     * location.
     *
     * @param routeLocation The RouteLocation.
     * @return The number of empty cars in the train departing the route location.
     */
    public int getNumberEmptyCarsInTrain(RouteLocation routeLocation) {
        int number = 0;
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
                    if (!car.getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
                        continue;
                    }
                    if (car.getRouteLocation() == rl) {
                        number++;
                    }
                    if (car.getRouteDestination() == rl) {
                        number--;
                    }
                }
                if (rl == routeLocation) {
                    break;
                }
            }
        }

        return number;
    }

    public int getNumberLoadedCarsInTrain(RouteLocation routeLocation) {
        return getNumberCarsInTrain(routeLocation) - getNumberEmptyCarsInTrain(routeLocation);
    }

    /**
     * Gets the number of cars pulled from a location
     *
     * @param routeLocation the location
     * @return number of pick ups
     */
    public int getNumberCarsPickedUp(RouteLocation routeLocation) {
        int number = 0;
        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
            if (rs.getRouteLocation() == routeLocation) {
                number++;
            }
        }
        return number;
    }

    /**
     * Gets the number of cars delivered to a location
     *
     * @param routeLocation the location
     * @return number of set outs
     */
    public int getNumberCarsSetout(RouteLocation routeLocation) {
        int number = 0;
        for (Car rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
            if (rs.getRouteDestination() == routeLocation) {
                number++;
            }
        }
        return number;
    }

    /**
     * Gets the train's length at the current location in the train's route.
     *
     * @return The train length at the train's current location
     */
    public int getTrainLength() {
        return getTrainLength(getCurrentRouteLocation());
    }

    /**
     * Gets the train's length at the route location specified
     *
     * @param routeLocation The route location
     * @return The train length at the route location
     */
    public int getTrainLength(RouteLocation routeLocation) {
        int length = 0;
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                for (RollingStock rs : InstanceManager.getDefault(EngineManager.class).getList(this)) {
                    if (rs.getRouteLocation() == rl) {
                        length += rs.getTotalLength();
                    }
                    if (rs.getRouteDestination() == rl) {
                        length += -rs.getTotalLength();
                    }
                }
                for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList(this)) {
                    if (rs.getRouteLocation() == rl) {
                        length += rs.getTotalLength();
                    }
                    if (rs.getRouteDestination() == rl) {
                        length += -rs.getTotalLength();
                    }
                }
                if (rl == routeLocation) {
                    break;
                }
            }
        }
        return length;
    }

    /**
     * Get the train's weight at the current location.
     *
     * @return Train's weight in tons.
     */
    public int getTrainWeight() {
        return getTrainWeight(getCurrentRouteLocation());
    }

    public int getTrainWeight(RouteLocation routeLocation) {
        int weight = 0;
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                for (RollingStock rs : InstanceManager.getDefault(EngineManager.class).getList(this)) {
                    if (rs.getRouteLocation() == rl) {
                        weight += rs.getAdjustedWeightTons();
                    }
                    if (rs.getRouteDestination() == rl) {
                        weight += -rs.getAdjustedWeightTons();
                    }
                }
                for (Car car : InstanceManager.getDefault(CarManager.class).getList(this)) {
                    if (car.getRouteLocation() == rl) {
                        weight += car.getAdjustedWeightTons(); // weight depends
                                                               // on car load
                    }
                    if (car.getRouteDestination() == rl) {
                        weight += -car.getAdjustedWeightTons();
                    }
                }
                if (rl == routeLocation) {
                    break;
                }
            }
        }
        return weight;
    }

    /**
     * Gets the train's locomotive horsepower at the route location specified
     *
     * @param routeLocation The route location
     * @return The train's locomotive horsepower at the route location
     */
    public int getTrainHorsePower(RouteLocation routeLocation) {
        int hp = 0;
        Route route = getRoute();
        if (route != null) {
            for (RouteLocation rl : route.getLocationsBySequenceList()) {
                for (Engine eng : InstanceManager.getDefault(EngineManager.class).getList(this)) {
                    if (eng.getRouteLocation() == rl) {
                        hp += eng.getHpInteger();
                    }
                    if (eng.getRouteDestination() == rl) {
                        hp += -eng.getHpInteger();
                    }
                }
                if (rl == routeLocation) {
                    break;
                }
            }
        }
        return hp;
    }

    /**
     * Gets the current caboose road and number if there's one assigned to the
     * train.
     *
     * @return Road and number of caboose.
     */
    public String getCabooseRoadAndNumber() {
        String cabooseRoadNumber = NONE;
        RouteLocation rl = getCurrentRouteLocation();
        List<Car> cars = InstanceManager.getDefault(CarManager.class).getByTrainList(this);
        for (Car car : cars) {
            if (car.getRouteLocation() == rl && car.isCaboose()) {
                cabooseRoadNumber =
                        car.getRoadName().split(TrainCommon.HYPHEN)[0] + " " + TrainCommon.splitString(car.getNumber());
            }
        }
        return cabooseRoadNumber;
    }

    public void setDescription(String description) {
        String old = _description;
        _description = description;
        if (!old.equals(description)) {
            setDirtyAndFirePropertyChange(DESCRIPTION_CHANGED_PROPERTY, old, description);
        }
    }

    public String getRawDescription() {
        return _description;
    }

    /**
     * Returns a formated string providing the train's description. {0} = lead
     * engine number, {1} = train's departure direction {2} = lead engine road {3} =
     * DCC address of lead engine.
     *
     * @return The train's description.
     */
    public String getDescription() {
        try {
            String description = MessageFormat.format(getRawDescription(), new Object[]{getLeadEngineNumber(),
                    getTrainDepartsDirection(), getLeadEngineRoadName(), getLeadEngineDccAddress()});
            return description;
        } catch (IllegalArgumentException e) {
            return "ERROR IN FORMATTING: " + getRawDescription();
        }
    }

    public void setNumberEngines(String number) {
        String old = _numberEngines;
        _numberEngines = number;
        if (!old.equals(number)) {
            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
        }
    }

    /**
     * Get the number of engines that this train requires.
     *
     * @return The number of engines that this train requires.
     */
    public String getNumberEngines() {
        return _numberEngines;
    }

    /**
     * Get the number of engines needed for the second set.
     *
     * @return The number of engines needed in route
     */
    public String getSecondLegNumberEngines() {
        return _leg2Engines;
    }

    public void setSecondLegNumberEngines(String number) {
        String old = _leg2Engines;
        _leg2Engines = number;
        if (!old.equals(number)) {
            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
        }
    }

    /**
     * Get the number of engines needed for the third set.
     *
     * @return The number of engines needed in route
     */
    public String getThirdLegNumberEngines() {
        return _leg3Engines;
    }

    public void setThirdLegNumberEngines(String number) {
        String old = _leg3Engines;
        _leg3Engines = number;
        if (!old.equals(number)) {
            setDirtyAndFirePropertyChange("trainNmberEngines", old, number); // NOI18N
        }
    }

    /**
     * Set the road name of engines servicing this train.
     *
     * @param road The road name of engines servicing this train.
     */
    public void setEngineRoad(String road) {
        String old = _engineRoad;
        _engineRoad = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
        }
    }

    /**
     * Get the road name of engines servicing this train.
     *
     * @return The road name of engines servicing this train.
     */
    public String getEngineRoad() {
        return _engineRoad;
    }

    /**
     * Set the road name of engines servicing this train 2nd leg.
     *
     * @param road The road name of engines servicing this train.
     */
    public void setSecondLegEngineRoad(String road) {
        String old = _leg2Road;
        _leg2Road = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
        }
    }

    /**
     * Get the road name of engines servicing this train 2nd leg.
     *
     * @return The road name of engines servicing this train.
     */
    public String getSecondLegEngineRoad() {
        return _leg2Road;
    }

    /**
     * Set the road name of engines servicing this train 3rd leg.
     *
     * @param road The road name of engines servicing this train.
     */
    public void setThirdLegEngineRoad(String road) {
        String old = _leg3Road;
        _leg3Road = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainEngineRoad", old, road); // NOI18N
        }
    }

    /**
     * Get the road name of engines servicing this train 3rd leg.
     *
     * @return The road name of engines servicing this train.
     */
    public String getThirdLegEngineRoad() {
        return _leg3Road;
    }

    /**
     * Set the model name of engines servicing this train.
     *
     * @param model The model name of engines servicing this train.
     */
    public void setEngineModel(String model) {
        String old = _engineModel;
        _engineModel = model;
        if (!old.equals(model)) {
            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
        }
    }

    public String getEngineModel() {
        return _engineModel;
    }

    /**
     * Set the model name of engines servicing this train's 2nd leg.
     *
     * @param model The model name of engines servicing this train.
     */
    public void setSecondLegEngineModel(String model) {
        String old = _leg2Model;
        _leg2Model = model;
        if (!old.equals(model)) {
            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
        }
    }

    public String getSecondLegEngineModel() {
        return _leg2Model;
    }

    /**
     * Set the model name of engines servicing this train's 3rd leg.
     *
     * @param model The model name of engines servicing this train.
     */
    public void setThirdLegEngineModel(String model) {
        String old = _leg3Model;
        _leg3Model = model;
        if (!old.equals(model)) {
            setDirtyAndFirePropertyChange("trainEngineModel", old, model); // NOI18N
        }
    }

    public String getThirdLegEngineModel() {
        return _leg3Model;
    }

    protected void replaceModel(String oldModel, String newModel) {
        if (getEngineModel().equals(oldModel)) {
            setEngineModel(newModel);
        }
        if (getSecondLegEngineModel().equals(oldModel)) {
            setSecondLegEngineModel(newModel);
        }
        if (getThirdLegEngineModel().equals(oldModel)) {
            setThirdLegEngineModel(newModel);
        }
    }

    /**
     * Set the road name of the caboose servicing this train.
     *
     * @param road The road name of the caboose servicing this train.
     */
    public void setCabooseRoad(String road) {
        String old = _cabooseRoad;
        _cabooseRoad = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
        }
    }

    public String getCabooseRoad() {
        return _cabooseRoad;
    }

    /**
     * Set the road name of the second leg caboose servicing this train.
     *
     * @param road The road name of the caboose servicing this train's 2nd leg.
     */
    public void setSecondLegCabooseRoad(String road) {
        String old = _leg2CabooseRoad;
        _leg2CabooseRoad = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
        }
    }

    public String getSecondLegCabooseRoad() {
        return _leg2CabooseRoad;
    }

    /**
     * Set the road name of the third leg caboose servicing this train.
     *
     * @param road The road name of the caboose servicing this train's 3rd leg.
     */
    public void setThirdLegCabooseRoad(String road) {
        String old = _leg3CabooseRoad;
        _leg3CabooseRoad = road;
        if (!old.equals(road)) {
            setDirtyAndFirePropertyChange("trainCabooseRoad", old, road); // NOI18N
        }
    }

    public String getThirdLegCabooseRoad() {
        return _leg3CabooseRoad;
    }

    public void setSecondLegStartRouteLocation(RouteLocation rl) {
        _leg2Start = rl;
    }

    public RouteLocation getSecondLegStartRouteLocation() {
        return _leg2Start;
    }

    public String getSecondLegStartLocationName() {
        if (getSecondLegStartRouteLocation() == null) {
            return NONE;
        }
        return getSecondLegStartRouteLocation().getName();
    }

    public void setThirdLegStartRouteLocation(RouteLocation rl) {
        _leg3Start = rl;
    }

    public RouteLocation getThirdLegStartRouteLocation() {
        return _leg3Start;
    }

    public String getThirdLegStartLocationName() {
        if (getThirdLegStartRouteLocation() == null) {
            return NONE;
        }
        return getThirdLegStartRouteLocation().getName();
    }

    public void setSecondLegEndRouteLocation(RouteLocation rl) {
        _end2Leg = rl;
    }

    public String getSecondLegEndLocationName() {
        if (getSecondLegEndRouteLocation() == null) {
            return NONE;
        }
        return getSecondLegEndRouteLocation().getName();
    }

    public RouteLocation getSecondLegEndRouteLocation() {
        return _end2Leg;
    }

    public void setThirdLegEndRouteLocation(RouteLocation rl) {
        _leg3End = rl;
    }

    public RouteLocation getThirdLegEndRouteLocation() {
        return _leg3End;
    }

    public String getThirdLegEndLocationName() {
        if (getThirdLegEndRouteLocation() == null) {
            return NONE;
        }
        return getThirdLegEndRouteLocation().getName();
    }

    /**
     * Optional changes to train while en route.
     *
     * @param options NO_CABOOSE_OR_FRED, CHANGE_ENGINES, ADD_CABOOSE,
     *                HELPER_ENGINES, REMOVE_CABOOSE
     */
    public void setSecondLegOptions(int options) {
        int old = _leg2Options;
        _leg2Options = options;
        if (old != options) {
            setDirtyAndFirePropertyChange("trainLegOptions", old, options); // NOI18N
        }
    }

    public int getSecondLegOptions() {
        return _leg2Options;
    }

    /**
     * Optional changes to train while en route.
     *
     * @param options NO_CABOOSE_OR_FRED, CHANGE_ENGINES, ADD_CABOOSE,
     *                HELPER_ENGINES, REMOVE_CABOOSE
     */
    public void setThirdLegOptions(int options) {
        int old = _leg3Options;
        _leg3Options = options;
        if (old != options) {
            setDirtyAndFirePropertyChange("trainLegOptions", old, options); // NOI18N
        }
    }

    public int getThirdLegOptions() {
        return _leg3Options;
    }

    public void setComment(String comment) {
        String old = _comment;
        _comment = comment;
        if (!old.equals(comment)) {
            setDirtyAndFirePropertyChange("trainComment", old, comment); // NOI18N
        }
    }
    
    public String getComment() {
        return TrainCommon.getTextColorString(getCommentWithColor());
    }

    public String getCommentWithColor() {
        return _comment;
    }

    /**
     * Add a script to run before a train is built
     *
     * @param pathname The script's pathname
     */
    public void addBuildScript(String pathname) {
        _buildScripts.add(pathname);
        setDirtyAndFirePropertyChange("addBuildScript", pathname, null); // NOI18N
    }

    public void deleteBuildScript(String pathname) {
        _buildScripts.remove(pathname);
        setDirtyAndFirePropertyChange("deleteBuildScript", null, pathname); // NOI18N
    }

    /**
     * Gets a list of pathnames (scripts) to run before this train is built
     *
     * @return A list of pathnames to run before this train is built
     */
    public List<String> getBuildScripts() {
        return _buildScripts;
    }

    /**
     * Add a script to run after a train is built
     *
     * @param pathname The script's pathname
     */
    public void addAfterBuildScript(String pathname) {
        _afterBuildScripts.add(pathname);
        setDirtyAndFirePropertyChange("addAfterBuildScript", pathname, null); // NOI18N
    }

    public void deleteAfterBuildScript(String pathname) {
        _afterBuildScripts.remove(pathname);
        setDirtyAndFirePropertyChange("deleteAfterBuildScript", null, pathname); // NOI18N
    }

    /**
     * Gets a list of pathnames (scripts) to run after this train is built
     *
     * @return A list of pathnames to run after this train is built
     */
    public List<String> getAfterBuildScripts() {
        return _afterBuildScripts;
    }

    /**
     * Add a script to run when train is moved
     *
     * @param pathname The script's pathname
     */
    public void addMoveScript(String pathname) {
        _moveScripts.add(pathname);
        setDirtyAndFirePropertyChange("addMoveScript", pathname, null); // NOI18N
    }

    public void deleteMoveScript(String pathname) {
        _moveScripts.remove(pathname);
        setDirtyAndFirePropertyChange("deleteMoveScript", null, pathname); // NOI18N
    }

    /**
     * Gets a list of pathnames (scripts) to run when this train moved
     *
     * @return A list of pathnames to run when this train moved
     */
    public List<String> getMoveScripts() {
        return _moveScripts;
    }

    /**
     * Add a script to run when train is terminated
     *
     * @param pathname The script's pathname
     */
    public void addTerminationScript(String pathname) {
        _terminationScripts.add(pathname);
        setDirtyAndFirePropertyChange("addTerminationScript", pathname, null); // NOI18N
    }

    public void deleteTerminationScript(String pathname) {
        _terminationScripts.remove(pathname);
        setDirtyAndFirePropertyChange("deleteTerminationScript", null, pathname); // NOI18N
    }

    /**
     * Gets a list of pathnames (scripts) to run when this train terminates
     *
     * @return A list of pathnames to run when this train terminates
     */
    public List<String> getTerminationScripts() {
        return _terminationScripts;
    }

    /**
     * Gets the optional railroad name for this train.
     *
     * @return Train's railroad name.
     */
    public String getRailroadName() {
        return _railroadName;
    }

    /**
     * Overrides the default railroad name for this train.
     *
     * @param name The railroad name for this train.
     */
    public void setRailroadName(String name) {
        String old = _railroadName;
        _railroadName = name;
        if (!old.equals(name)) {
            setDirtyAndFirePropertyChange("trainRailroadName", old, name); // NOI18N
        }
    }

    public String getManifestLogoPathName() {
        return _logoPathName;
    }

    /**
     * Overrides the default logo for this train.
     *
     * @param pathName file location for the logo.
     */
    public void setManifestLogoPathName(String pathName) {
        _logoPathName = pathName;
    }

    public boolean isShowArrivalAndDepartureTimesEnabled() {
        return _showTimes;
    }

    public void setShowArrivalAndDepartureTimes(boolean enable) {
        boolean old = _showTimes;
        _showTimes = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("showArrivalAndDepartureTimes", old ? "true" : "false", // NOI18N
                    enable ? "true" : "false"); // NOI18N
        }
    }

    public boolean isSendCarsToTerminalEnabled() {
        return _sendToTerminal;
    }

    public void setSendCarsToTerminalEnabled(boolean enable) {
        boolean old = _sendToTerminal;
        _sendToTerminal = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("send cars to terminal", old ? "true" : "false", enable ? "true" // NOI18N
                    : "false"); // NOI18N
        }
    }

    /**
     * Allow local moves if car has a custom load or Final Destination
     *
     * @return true if local move is allowed
     */
    public boolean isAllowLocalMovesEnabled() {
        return _allowLocalMoves;
    }

    public void setAllowLocalMovesEnabled(boolean enable) {
        boolean old = _allowLocalMoves;
        _allowLocalMoves = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("allow local moves", old ? "true" : "false", enable ? "true" // NOI18N
                    : "false"); // NOI18N
        }
    }

    public boolean isAllowThroughCarsEnabled() {
        return _allowThroughCars;
    }

    public void setAllowThroughCarsEnabled(boolean enable) {
        boolean old = _allowThroughCars;
        _allowThroughCars = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("allow through cars", old ? "true" : "false", enable ? "true" // NOI18N
                    : "false"); // NOI18N
        }
    }

    public boolean isBuildTrainNormalEnabled() {
        return _buildNormal;
    }

    public void setBuildTrainNormalEnabled(boolean enable) {
        boolean old = _buildNormal;
        _buildNormal = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("build train normal", old ? "true" : "false", enable ? "true" // NOI18N
                    : "false"); // NOI18N
        }
    }

    /**
     * When true allow a turn to return cars to staging. A turn is a train that
     * departs and terminates at the same location.
     *
     * @return true if cars can return to staging
     */
    public boolean isAllowReturnToStagingEnabled() {
        return _allowCarsReturnStaging;
    }

    public void setAllowReturnToStagingEnabled(boolean enable) {
        boolean old = _allowCarsReturnStaging;
        _allowCarsReturnStaging = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("allow cars to return to staging", old ? "true" : "false", // NOI18N
                    enable ? "true" : "false"); // NOI18N
        }
    }

    public boolean isServiceAllCarsWithFinalDestinationsEnabled() {
        return _serviceAllCarsWithFinalDestinations;
    }

    public void setServiceAllCarsWithFinalDestinationsEnabled(boolean enable) {
        boolean old = _serviceAllCarsWithFinalDestinations;
        _serviceAllCarsWithFinalDestinations = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("TrainServiceAllCarsWithFinalDestinations", old ? "true" : "false", // NOI18N
                    enable ? "true" : "false"); // NOI18N
        }
    }

    public boolean isBuildConsistEnabled() {
        return _buildConsist;
    }

    public void setBuildConsistEnabled(boolean enable) {
        boolean old = _buildConsist;
        _buildConsist = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("TrainBuildConsist", old ? "true" : "false", // NOI18N
                    enable ? "true" : "false"); // NOI18N
        }
    }

    public boolean isSendCarsWithCustomLoadsToStagingEnabled() {
        return _sendCarsWithCustomLoadsToStaging;
    }

    public void setSendCarsWithCustomLoadsToStagingEnabled(boolean enable) {
        boolean old = _sendCarsWithCustomLoadsToStaging;
        _sendCarsWithCustomLoadsToStaging = enable;
        if (old != enable) {
            setDirtyAndFirePropertyChange("SendCarsWithCustomLoadsToStaging", old ? "true" : "false", // NOI18N
                    enable ? "true" : "false"); // NOI18N
        }
    }

    protected void setBuilt(boolean built) {
        boolean old = _built;
        _built = built;
        if (old != built) {
            setDirtyAndFirePropertyChange(BUILT_CHANGED_PROPERTY, old, built); // NOI18N
        }
    }

    /**
     * Used to determine if this train has been built.
     *
     * @return true if the train was successfully built.
     */
    public boolean isBuilt() {
        return _built;
    }

    /**
     * Set true whenever the train's manifest has been modified. For example adding
     * or removing a car from a train, or changing the manifest format. Once the
     * manifest has been regenerated (modified == false), the old status for the
     * train is restored.
     *
     * @param modified True if train's manifest has been modified.
     */
    public void setModified(boolean modified) {
        log.debug("Set modified {}", modified);
        if (!isBuilt()) {
            _modified = false;
            return; // there isn't a manifest to modify
        }
        boolean old = _modified;
        _modified = modified;
        if (modified) {
            setPrinted(false);
        }
        if (old != modified) {
            if (modified) {
                // scripts can call setModified() for a train
                if (getStatusCode() != CODE_RUN_SCRIPTS) {
                    setOldStatusCode(getStatusCode());
                }
                setStatusCode(CODE_MANIFEST_MODIFIED);
            } else {
                setStatusCode(getOldStatusCode()); // restore previous train
                                                   // status
            }
        }
        setDirtyAndFirePropertyChange(TRAIN_MODIFIED_CHANGED_PROPERTY, null, modified); // NOI18N
    }

    public boolean isModified() {
        return _modified;
    }

    /**
     * Control flag used to decide if this train is to be built.
     *
     * @param build When true, build this train.
     */
    public void setBuildEnabled(boolean build) {
        boolean old = _build;
        _build = build;
        if (old != build) {
            setDirtyAndFirePropertyChange(BUILD_CHANGED_PROPERTY, old, build); // NOI18N
        }
    }

    /**
     * Used to determine if train is to be built.
     *
     * @return true if train is to be built.
     */
    public boolean isBuildEnabled() {
        return _build;
    }

    /**
     * Build this train if the build control flag is true.
     *
     * @return True only if train is successfully built.
     */
    public boolean buildIfSelected() {
        if (isBuildEnabled() && !isBuilt()) {
            return build();
        }
        log.debug("Train ({}) not selected or already built, skipping build", getName());
        return false;
    }

    /**
     * Build this train. Creates a train manifest.
     *
     * @return True if build successful.
     */
    public synchronized boolean build() {
        reset();
        // check to see if any other trains are building
        while (InstanceManager.getDefault(TrainManager.class).isAnyTrainBuilding()) {
            try {
                wait(100); // 100 msec
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                log.error("Thread unexpectedly interrupted", e);
            }
        }
        // run before build scripts
        runScripts(getBuildScripts());
        TrainBuilder tb = new TrainBuilder();
        boolean results = tb.build(this);
        // run after build scripts
        runScripts(getAfterBuildScripts());
        return results;
    }

    /**
     * Run train scripts, waits for completion before returning.
     */
    private synchronized void runScripts(List<String> scripts) {
        if (scripts.size() > 0) {
            // save the current status
            setOldStatusCode(getStatusCode());
            setStatusCode(CODE_RUN_SCRIPTS);
            // create the python interpreter thread
            JmriScriptEngineManager.getDefault().initializeAllEngines();
            // find the number of active threads
            ThreadGroup root = Thread.currentThread().getThreadGroup();
            int numberOfThreads = root.activeCount();
            // log.debug("Number of active threads: {}", numberOfThreads);
            for (String scriptPathname : scripts) {
                try {
                    JmriScriptEngineManager.getDefault()
                            .runScript(new File(jmri.util.FileUtil.getExternalFilename(scriptPathname)));
                } catch (Exception e) {
                    log.error("Problem with script: {}", scriptPathname);
                }
            }
            // need to wait for scripts to complete or 4 seconds maximum
            int count = 0;
            while (root.activeCount() > numberOfThreads) {
                log.debug("Number of active threads: {}, at start: {}", root.activeCount(), numberOfThreads);
                try {
                    wait(40);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                if (count++ > 100) {
                    break; // 4 seconds maximum 40*100 = 4000
                }
            }
            setStatusCode(getOldStatusCode());
        }
    }

    public boolean printBuildReport() {
        boolean isPreview = (InstanceManager.getDefault(TrainManager.class).isPrintPreviewEnabled() ||
                Setup.isBuildReportAlwaysPreviewEnabled());
        return printBuildReport(isPreview);
    }

    public boolean printBuildReport(boolean isPreview) {
        File buildFile = InstanceManager.getDefault(TrainManagerXml.class).getTrainBuildReportFile(getName());
        if (!buildFile.exists()) {
            log.warn("Build file missing for train {}", getName());
            return false;
        }

        if (isPreview && Setup.isBuildReportEditorEnabled()) {
            TrainPrintUtilities.editReport(buildFile, getName());
        } else {
            TrainPrintUtilities.printReport(buildFile,
                    Bundle.getMessage("buildReport", getDescription()),
                    isPreview, NONE, true, NONE, NONE, Setup.PORTRAIT, Setup.getBuildReportFontSize(), true);
        }
        return true;
    }

    protected void setBuildFailed(boolean status) {
        boolean old = _buildFailed;
        _buildFailed = status;
        if (old != status) {
            setDirtyAndFirePropertyChange("buildFailed", old ? "true" : "false", status ? "true" : "false"); // NOI18N
        }
    }

    /**
     * Returns true if the train build failed. Note that returning false doesn't
     * mean the build was successful.
     *
     * @return true if train build failed.
     */
    public boolean isBuildFailed() {
        return _buildFailed;
    }

    protected void setBuildFailedMessage(String message) {
        String old = _buildFailedMessage;
        _buildFailedMessage = message;
        if (!old.equals(message)) {
            setDirtyAndFirePropertyChange("buildFailedMessage", old, message); // NOI18N
        }
    }

    protected String getBuildFailedMessage() {
        return _buildFailedMessage;
    }

    /**
     * Print manifest for train if already built.
     *
     * @return true if print successful.
     */
    public boolean printManifestIfBuilt() {
        if (isBuilt()) {
            boolean isPreview = InstanceManager.getDefault(TrainManager.class).isPrintPreviewEnabled();
            return (printManifest(isPreview));
        } else {
            log.debug("Need to build train ({}) before printing manifest", getName());
            return false;
        }
    }

    /**
     * Print manifest for train.
     *
     * @param isPreview True if preview.
     * @return true if print successful, false if train print file not found.
     */
    public boolean printManifest(boolean isPreview) {
        if (isModified()) {
            new TrainManifest(this);
            try {
                new JsonManifest(this).build();
            } catch (IOException ex) {
                log.error("Unable to create JSON manifest {}", ex.getLocalizedMessage());
            }
            new TrainCsvManifest(this);
        }
        File file = InstanceManager.getDefault(TrainManagerXml.class).getTrainManifestFile(getName());
        if (!file.exists()) {
            log.warn("Manifest file missing for train ({})", getName());
            return false;
        }
        if (isPreview && Setup.isManifestEditorEnabled()) {
            TrainUtilities.openDesktop(file);
            return true;
        }
        String logoURL = Setup.NONE;
        if (!getManifestLogoPathName().equals(NONE)) {
            logoURL = FileUtil.getExternalFilename(getManifestLogoPathName());
        } else if (!Setup.getManifestLogoURL().equals(Setup.NONE)) {
            logoURL = FileUtil.getExternalFilename(Setup.getManifestLogoURL());
        }
        Location departs = InstanceManager.getDefault(LocationManager.class).getLocationByName(getTrainDepartsName());
        String printerName = Location.NONE;
        if (departs != null) {
            printerName = departs.getDefaultPrinterName();
        }
        // the train description shouldn't exceed half of the page width or the
        // page number will be overwritten
        String name = getDescription();
        if (name.length() > TrainCommon.getManifestHeaderLineLength() / 2) {
            name = name.substring(0, TrainCommon.getManifestHeaderLineLength() / 2);
        }
        TrainPrintUtilities.printReport(file, name, isPreview, Setup.getFontName(), false, logoURL, printerName,
                Setup.getManifestOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled());
        if (!isPreview) {
            setPrinted(true);
        }
        return true;
    }

    public boolean openFile() {
        File file = createCsvManifestFile();
        if (file == null || !file.exists()) {
            log.warn("CSV manifest file missing for train {}", getName());
            return false;
        }
        TrainUtilities.openDesktop(file);
        return true;
    }

    public boolean runFile() {
        File file = createCsvManifestFile();
        if (file == null || !file.exists()) {
            log.warn("CSV manifest file missing for train {}", getName());
            return false;
        }
        // Set up to process the CSV file by the external Manifest program
        InstanceManager.getDefault(TrainCustomManifest.class).addCsvFile(file);
        if (!InstanceManager.getDefault(TrainCustomManifest.class).process()) {
            if (!InstanceManager.getDefault(TrainCustomManifest.class).excelFileExists()) {
                JmriJOptionPane.showMessageDialog(null,
                        Bundle.getMessage("LoadDirectoryNameFileName",
                                InstanceManager.getDefault(TrainCustomManifest.class).getDirectoryPathName(),
                                        InstanceManager.getDefault(TrainCustomManifest.class).getFileName()),
                        Bundle.getMessage("ManifestCreatorNotFound"), JmriJOptionPane.ERROR_MESSAGE);
            }
            return false;
        }
        return true;
    }

    public File createCsvManifestFile() {
        if (isModified()) {
            new TrainManifest(this);
            try {
                new JsonManifest(this).build();
            } catch (IOException ex) {
                log.error("Unable to create JSON manifest {}", ex.getLocalizedMessage());
            }
            new TrainCsvManifest(this);
        }
        File file = InstanceManager.getDefault(TrainManagerXml.class).getTrainCsvManifestFile(getName());
        if (!file.exists()) {
            log.warn("CSV manifest file was not created for train ({})", getName());
            return null;
        }
        return file;
    }

    public void setPrinted(boolean printed) {
        boolean old = _printed;
        _printed = printed;
        if (old != printed) {
            setDirtyAndFirePropertyChange("trainPrinted", old ? "true" : "false", printed ? "true" : "false"); // NOI18N
        }
    }

    /**
     * Used to determine if train manifest was printed.
     *
     * @return true if the train manifest was printed.
     */
    public boolean isPrinted() {
        return _printed;
    }

    /**
     * Sets the panel position for the train icon for the current route location.
     *
     * @return true if train coordinates can be set
     */
    public boolean setTrainIconCoordinates() {
        if (Setup.isTrainIconCordEnabled() && getCurrentRouteLocation() != null && _trainIcon != null) {
            getCurrentRouteLocation().setTrainIconX(_trainIcon.getX());
            getCurrentRouteLocation().setTrainIconY(_trainIcon.getY());
            return true;
        }
        return false;
    }

    /**
     * Terminate train.
     */
    public void terminate() {
        while (isBuilt()) {
            move();
        }
    }

    /**
     * Move train to next location in the route. Will move engines, cars, and train
     * icon. Will also terminate a train after it arrives at its final destination.
     */
    public void move() {
        log.debug("Move train ({})", getName());
        if (getRoute() == null || getCurrentRouteLocation() == null) {
            setBuilt(false); // break terminate loop
            return;
        }
        if (!isBuilt()) {
            log.error("ERROR attempt to move train ({}) that hasn't been built", getName());
            return;
        }
        RouteLocation rl = getCurrentRouteLocation();
        RouteLocation rlNext = getNextRouteLocation(rl);

        setCurrentLocation(rlNext);

        // cars and engines will move via property change
        setDirtyAndFirePropertyChange(TRAIN_LOCATION_CHANGED_PROPERTY, rl, rlNext);
        moveTrainIcon(rlNext);
        updateStatus(rl, rlNext);
        // tell GUI that train has complete its move
        setDirtyAndFirePropertyChange(TRAIN_MOVE_COMPLETE_CHANGED_PROPERTY, rl, rlNext);
    }

    /**
     * Move train to a location in the train's route. Code checks to see if the
     * location requested is part of the train's route and if the train hasn't
     * already visited the location. This command can only move the train forward in
     * its route. Note that you can not terminate the train using this command. See
     * move() or terminate().
     *
     * @param locationName The name of the location to move this train.
     * @return true if train was able to move to the named location.
     */
    public boolean move(String locationName) {
        log.info("Move train ({}) to location ({})", getName(), locationName);
        if (getRoute() == null || getCurrentRouteLocation() == null) {
            return false;
        }
        List<RouteLocation> routeList = getRoute().getLocationsBySequenceList();
        for (int i = 0; i < routeList.size(); i++) {
            RouteLocation rl = routeList.get(i);
            if (getCurrentRouteLocation() == rl) {
                for (int j = i + 1; j < routeList.size(); j++) {
                    rl = routeList.get(j);
                    if (rl.getName().equals(locationName)) {
                        log.debug("Found location ({}) moving train to this location", locationName);
                        for (j = i + 1; j < routeList.size(); j++) {
                            rl = routeList.get(j);
                            move();
                            if (rl.getName().equals(locationName)) {
                                return true;
                            }
                        }
                    }
                }
                break; // done
            }
        }
        return false;
    }

    /**
     * Moves the train to the specified route location
     *
     * @param rl route location
     * @return true if successful
     */
    public boolean move(RouteLocation rl) {
        if (rl == null) {
            return false;
        }
        log.debug("Move train ({}) to location ({})", getName(), rl.getName());
        if (getRoute() == null || getCurrentRouteLocation() == null) {
            return false;
        }
        boolean foundCurrent = false;
        for (RouteLocation xrl : getRoute().getLocationsBySequenceList()) {
            if (getCurrentRouteLocation() == xrl) {
                foundCurrent = true;
            }
            if (xrl == rl) {
                if (foundCurrent) {
                    return true; // done
                } else {
                    break; // train passed this location
                }
            }
            if (foundCurrent) {
                move();
            }
        }
        return false;
    }

    /**
     * Move train to the next location in the train's route. The location name
     * provided must be equal to the next location name in the train's route.
     *
     * @param locationName The next location name in the train's route.
     * @return true if successful.
     */
    public boolean moveToNextLocation(String locationName) {
        if (getNextLocationName().equals(locationName)) {
            move();
            return true;
        }
        return false;
    }

    public void loadTrainIcon() {
        if (getCurrentRouteLocation() != null) {
            moveTrainIcon(getCurrentRouteLocation());
        }
    }

    private final boolean animation = true; // when true use animation for icon
                                            // moves
    TrainIconAnimation _ta;

    /*
     * The train icon is moved to route location (rl) for this train
     */
    protected void moveTrainIcon(RouteLocation rl) {
        // create train icon if at departure, if program has been restarted, or removed
        if (rl == getTrainDepartsRouteLocation() || _trainIcon == null || !_trainIcon.isActive()) {
            createTrainIcon(rl);
        }
        // is the lead engine still in train
        if (getLeadEngine() != null && getLeadEngine().getRouteDestination() == rl && rl != null) {
            log.debug("Engine ({}) arriving at destination {}", getLeadEngine().toString(), rl.getName());
        }
        if (_trainIcon != null && _trainIcon.isActive()) {
            setTrainIconColor();
            _trainIcon.setShowToolTip(true);
            String txt = null;
            if (getCurrentLocationName().equals(NONE)) {
                txt = getDescription() + " " + Bundle.getMessage("Terminated") + " (" + getTrainTerminatesName() + ")";
            } else {
                txt = Bundle.getMessage("TrainAtNext",
                        getDescription(), getCurrentLocationName(), getNextLocationName(), getTrainLength(),
                        Setup.getLengthUnit().toLowerCase());
            }
            _trainIcon.getToolTip().setText(txt);
            _trainIcon.getToolTip().setBackgroundColor(Color.white);
            // rl can be null when train is terminated.
            if (rl != null) {
                if (rl.getTrainIconX() != 0 || rl.getTrainIconY() != 0) {
                    if (animation) {
                        TrainIconAnimation ta = new TrainIconAnimation(_trainIcon, rl, _ta);
                        ta.start(); // start the animation
                        _ta = ta;
                    } else {
                        _trainIcon.setLocation(rl.getTrainIconX(), rl.getTrainIconY());
                    }
                }
            }
        }
    }

    public String getIconName() {
        String name = getName();
        if (isBuilt() && getLeadEngine() != null && Setup.isTrainIconAppendEnabled()) {
            name += " " + getLeadEngine().getNumber();
        }
        return name;
    }

    public String getLeadEngineNumber() {
        if (getLeadEngine() == null) {
            return NONE;
        }
        return getLeadEngine().getNumber();
    }

    public String getLeadEngineRoadName() {
        if (getLeadEngine() == null) {
            return NONE;
        }
        return getLeadEngine().getRoadName();
    }

    public String getLeadEngineRoadAndNumber() {
        if (getLeadEngine() == null) {
            return NONE;
        }
        return getLeadEngine().toString();
    }

    public String getLeadEngineDccAddress() {
        if (getLeadEngine() == null) {
            return NONE;
        }
        return getLeadEngine().getDccAddress();
    }

    /**
     * Gets the lead engine, will create it if the program has been restarted
     *
     * @return lead engine for this train
     */
    public Engine getLeadEngine() {
        if (_leadEngine == null && !_leadEngineId.equals(NONE)) {
            _leadEngine = InstanceManager.getDefault(EngineManager.class).getById(_leadEngineId);
        }
        return _leadEngine;
    }

    public void setLeadEngine(Engine engine) {
        if (engine == null) {
            _leadEngineId = NONE;
        }
        _leadEngine = engine;
    }

    /**
     * Returns the lead engine in a train's route. There can be up to two changes in
     * the lead engine for a train.
     *
     * @param routeLocation where in the train's route to find the lead engine.
     * @return lead engine
     */
    public Engine getLeadEngine(RouteLocation routeLocation) {
        Engine lead = null;
        for (RouteLocation rl : getRoute().getLocationsBySequenceList()) {
            for (Engine engine : InstanceManager.getDefault(EngineManager.class).getByTrainList(this)) {
                if (engine.getRouteLocation() == rl && (engine.getConsist() == null || engine.isLead())) {
                    lead = engine;
                    break;
                }
            }
            if (rl == routeLocation) {
                break;
            }
        }
        return lead;
    }

    protected TrainIcon _trainIcon = null;

    public TrainIcon getTrainIcon() {
        return _trainIcon;
    }

    public void createTrainIcon(RouteLocation rl) {
        if (_trainIcon != null && _trainIcon.isActive()) {
            _trainIcon.remove();
        }
        // if there's a panel specified, get it and place icon
        if (!Setup.getPanelName().isEmpty()) {
            Editor editor = InstanceManager.getDefault(EditorManager.class).getTargetFrame(Setup.getPanelName());
            if (editor != null) {
                try {
                    _trainIcon = editor.addTrainIcon(getIconName());
                } catch (Exception e) {
                    log.error("Error placing train ({}) icon on panel ({})", getName(), Setup.getPanelName(), e);
                    return;
                }
                _trainIcon.setTrain(this);
                if (getIconName().length() > 9) {
                    _trainIcon.setFont(_trainIcon.getFont().deriveFont(8.f));
                }
                if (rl != null) {
                    _trainIcon.setLocation(rl.getTrainIconX(), rl.getTrainIconY());
                }
                // add throttle if there's a throttle manager
                if (jmri.InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
                    // add throttle if JMRI loco roster entry exist
                    RosterEntry entry = null;
                    if (getLeadEngine() != null) {
                        // first try and find a match based on loco road number
                        entry = getLeadEngine().getRosterEntry();
                    }
                    if (entry != null) {
                        _trainIcon.setRosterEntry(entry);
                        if (getLeadEngine().getConsist() != null) {
                            _trainIcon.setConsistNumber(getLeadEngine().getConsist().getConsistNumber());
                        }
                    } else {
                        log.debug("Loco roster entry not found for train ({})", getName());
                    }
                }
            }
        }
    }

    private void setTrainIconColor() {
        // Terminated train?
        if (getCurrentLocationName().equals(NONE)) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorTerminate());
            return;
        }
        // local train serving only one location?
        if (isLocalSwitcher()) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorLocal());
            return;
        }
        // set color based on train direction at current location
        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.NORTH) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorNorth());
        }
        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.SOUTH) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorSouth());
        }
        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.EAST) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorEast());
        }
        if (getCurrentRouteLocation().getTrainDirection() == RouteLocation.WEST) {
            _trainIcon.setLocoColor(Setup.getTrainIconColorWest());
        }
    }

    private void updateStatus(RouteLocation old, RouteLocation next) {
        if (next != null) {
            setStatusCode(CODE_TRAIN_EN_ROUTE);
            // run move scripts
            runScripts(getMoveScripts());
        } else {
            log.debug("Train ({}) terminated", getName());
            setTerminationDate(TrainCommon.getDate(false));
            setStatusCode(CODE_TERMINATED);
            setBuilt(false);
            // run termination scripts
            runScripts(getTerminationScripts());
        }
    }

    /**
     * Sets the print status for switch lists
     *
     * @param status UNKNOWN PRINTED
     */
    public void setSwitchListStatus(String status) {
        String old = _switchListStatus;
        _switchListStatus = status;
        if (!old.equals(status)) {
            setDirtyAndFirePropertyChange("switch list train status", old, status); // NOI18N
        }
    }

    public String getSwitchListStatus() {
        return _switchListStatus;
    }

    /**
     * Resets the train, removes engines and cars from this train.
     *
     * @return true if reset successful
     */
    public boolean reset() {
        // is this train in route?
        if (isTrainEnRoute()) {
            log.info("Train ({}) has started its route, can not be reset", getName());
            return false;
        }
        setCurrentLocation(null);
        setDepartureTrack(null);
        setTerminationTrack(null);
        setBuilt(false);
        setBuildFailed(false);
        setBuildFailedMessage(NONE);
        setPrinted(false);
        setModified(false);
        // remove cars and engines from this train via property change
        setStatusCode(CODE_TRAIN_RESET);
        // remove train icon
        if (_trainIcon != null && _trainIcon.isActive()) {
            _trainIcon.remove();
        }
        return true;
    }

    public void dispose() {
        if (getRoute() != null) {
            getRoute().removePropertyChangeListener(this);
        }
        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
        InstanceManager.getDefault(EngineTypes.class).removePropertyChangeListener(this);
        InstanceManager.getDefault(CarOwners.class).removePropertyChangeListener(this);
        InstanceManager.getDefault(EngineModels.class).removePropertyChangeListener(this);

        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, "Dispose"); // NOI18N
    }

    /**
     * Construct this Entry from XML. This member has to remain synchronized with
     * the detailed DTD in operations-trains.dtd
     *
     * @param e Consist XML element
     */
    public Train(Element e) {
        org.jdom2.Attribute a;
        if ((a = e.getAttribute(Xml.ID)) != null) {
            _id = a.getValue();
        } else {
            log.warn("no id attribute in train element when reading operations");
        }
        if ((a = e.getAttribute(Xml.NAME)) != null) {
            _name = a.getValue();
        }
        if ((a = e.getAttribute(Xml.DESCRIPTION)) != null) {
            _description = a.getValue();
        }
        if ((a = e.getAttribute(Xml.DEPART_HOUR)) != null) {
            String hour = a.getValue();
            if ((a = e.getAttribute(Xml.DEPART_MINUTE)) != null) {
                String minute = a.getValue();
                _departureTime = hour + ":" + minute;
            }
        }

        // Trains table row color
        Element eRowColor = e.getChild(Xml.ROW_COLOR);
        if (eRowColor != null && (a = eRowColor.getAttribute(Xml.NAME)) != null) {
            _tableRowColorName = a.getValue().toLowerCase();
        }
        if (eRowColor != null && (a = eRowColor.getAttribute(Xml.RESET_ROW_COLOR)) != null) {
            _tableRowColorResetName = a.getValue().toLowerCase();
        }

        Element eRoute = e.getChild(Xml.ROUTE);
        if (eRoute != null) {
            if ((a = eRoute.getAttribute(Xml.ID)) != null) {
                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteById(a.getValue()));
            }
            if (eRoute.getChild(Xml.SKIPS) != null) {
                List<Element> skips = eRoute.getChild(Xml.SKIPS).getChildren(Xml.LOCATION);
                String[] locs = new String[skips.size()];
                for (int i = 0; i < skips.size(); i++) {
                    Element loc = skips.get(i);
                    if ((a = loc.getAttribute(Xml.ID)) != null) {
                        locs[i] = a.getValue();
                    }
                }
                setTrainSkipsLocations(locs);
            }
        } else {
            // old format
            // try and first get the route by id then by name
            if ((a = e.getAttribute(Xml.ROUTE_ID)) != null) {
                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteById(a.getValue()));
            } else if ((a = e.getAttribute(Xml.ROUTE)) != null) {
                setRoute(InstanceManager.getDefault(RouteManager.class).getRouteByName(a.getValue()));
            }
            if ((a = e.getAttribute(Xml.SKIP)) != null) {
                String locationIds = a.getValue();
                String[] locs = locationIds.split("%%"); // NOI18N
                // log.debug("Train skips: {}", locationIds);
                setTrainSkipsLocations(locs);
            }
        }
        // new way of reading car types using elements
        if (e.getChild(Xml.TYPES) != null) {
            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
            String[] types = new String[carTypes.size()];
            for (int i = 0; i < carTypes.size(); i++) {
                Element type = carTypes.get(i);
                if ((a = type.getAttribute(Xml.NAME)) != null) {
                    types[i] = a.getValue();
                }
            }
            setTypeNames(types);
            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
            types = new String[locoTypes.size()];
            for (int i = 0; i < locoTypes.size(); i++) {
                Element type = locoTypes.get(i);
                if ((a = type.getAttribute(Xml.NAME)) != null) {
                    types[i] = a.getValue();
                }
            }
            setTypeNames(types);
        } // old way of reading car types up to version 2.99.6
        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
            String names = a.getValue();
            String[] types = names.split("%%"); // NOI18N
            // log.debug("Car types: {}", names);
            setTypeNames(types);
        }
        // old misspelled format
        if ((a = e.getAttribute(Xml.CAR_ROAD_OPERATION)) != null) {
            _carRoadOption = a.getValue();
        }
        if ((a = e.getAttribute(Xml.CAR_ROAD_OPTION)) != null) {
            _carRoadOption = a.getValue();
        }
        // new way of reading car roads using elements
        if (e.getChild(Xml.CAR_ROADS) != null) {
            List<Element> carRoads = e.getChild(Xml.CAR_ROADS).getChildren(Xml.CAR_ROAD);
            String[] roads = new String[carRoads.size()];
            for (int i = 0; i < carRoads.size(); i++) {
                Element road = carRoads.get(i);
                if ((a = road.getAttribute(Xml.NAME)) != null) {
                    roads[i] = a.getValue();
                }
            }
            setCarRoadNames(roads);
        } // old way of reading car roads up to version 2.99.6
        else if ((a = e.getAttribute(Xml.CAR_ROADS)) != null) {
            String names = a.getValue();
            String[] roads = names.split("%%"); // NOI18N
            log.debug("Train ({}) {} car roads: {}", getName(), getCarRoadOption(), names);
            setCarRoadNames(roads);
        }
        
        if ((a = e.getAttribute(Xml.LOCO_ROAD_OPTION)) != null) {
            _locoRoadOption = a.getValue();
        }
        // new way of reading engine roads using elements
        if (e.getChild(Xml.LOCO_ROADS) != null) {
            List<Element> locoRoads = e.getChild(Xml.LOCO_ROADS).getChildren(Xml.LOCO_ROAD);
            String[] roads = new String[locoRoads.size()];
            for (int i = 0; i < locoRoads.size(); i++) {
                Element road = locoRoads.get(i);
                if ((a = road.getAttribute(Xml.NAME)) != null) {
                    roads[i] = a.getValue();
                }
            }
            setLocoRoadNames(roads);
        }

        if ((a = e.getAttribute(Xml.CAR_LOAD_OPTION)) != null) {
            _loadOption = a.getValue();
        }
        if ((a = e.getAttribute(Xml.CAR_OWNER_OPTION)) != null) {
            _ownerOption = a.getValue();
        }
        if ((a = e.getAttribute(Xml.BUILT_START_YEAR)) != null) {
            _builtStartYear = a.getValue();
        }
        if ((a = e.getAttribute(Xml.BUILT_END_YEAR)) != null) {
            _builtEndYear = a.getValue();
        }
        // new way of reading car loads using elements
        if (e.getChild(Xml.CAR_LOADS) != null) {
            List<Element> carLoads = e.getChild(Xml.CAR_LOADS).getChildren(Xml.CAR_LOAD);
            String[] loads = new String[carLoads.size()];
            for (int i = 0; i < carLoads.size(); i++) {
                Element load = carLoads.get(i);
                if ((a = load.getAttribute(Xml.NAME)) != null) {
                    loads[i] = a.getValue();
                }
            }
            setLoadNames(loads);
        } // old way of reading car loads up to version 2.99.6
        else if ((a = e.getAttribute(Xml.CAR_LOADS)) != null) {
            String names = a.getValue();
            String[] loads = names.split("%%"); // NOI18N
            log.debug("Train ({}) {} car loads: {}", getName(), getLoadOption(), names);
            setLoadNames(loads);
        }
        // new way of reading car owners using elements
        if (e.getChild(Xml.CAR_OWNERS) != null) {
            List<Element> carOwners = e.getChild(Xml.CAR_OWNERS).getChildren(Xml.CAR_OWNER);
            String[] owners = new String[carOwners.size()];
            for (int i = 0; i < carOwners.size(); i++) {
                Element owner = carOwners.get(i);
                if ((a = owner.getAttribute(Xml.NAME)) != null) {
                    owners[i] = a.getValue();
                }
            }
            setOwnerNames(owners);
        } // old way of reading car owners up to version 2.99.6
        else if ((a = e.getAttribute(Xml.CAR_OWNERS)) != null) {
            String names = a.getValue();
            String[] owners = names.split("%%"); // NOI18N
            log.debug("Train ({}) {} car owners: {}", getName(), getOwnerOption(), names);
            setOwnerNames(owners);
        }

        if ((a = e.getAttribute(Xml.NUMBER_ENGINES)) != null) {
            _numberEngines = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG2_ENGINES)) != null) {
            _leg2Engines = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG3_ENGINES)) != null) {
            _leg3Engines = a.getValue();
        }
        if ((a = e.getAttribute(Xml.ENGINE_ROAD)) != null) {
            _engineRoad = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG2_ROAD)) != null) {
            _leg2Road = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG3_ROAD)) != null) {
            _leg3Road = a.getValue();
        }
        if ((a = e.getAttribute(Xml.ENGINE_MODEL)) != null) {
            _engineModel = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG2_MODEL)) != null) {
            _leg2Model = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG3_MODEL)) != null) {
            _leg3Model = a.getValue();
        }
        if ((a = e.getAttribute(Xml.REQUIRES)) != null) {
            try {
                _requires = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Requires ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        }
        if ((a = e.getAttribute(Xml.CABOOSE_ROAD)) != null) {
            _cabooseRoad = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG2_CABOOSE_ROAD)) != null) {
            _leg2CabooseRoad = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG3_CABOOSE_ROAD)) != null) {
            _leg3CabooseRoad = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEG2_OPTIONS)) != null) {
            try {
                _leg2Options = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Leg 2 options ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        }
        if ((a = e.getAttribute(Xml.LEG3_OPTIONS)) != null) {
            try {
                _leg3Options = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Leg 3 options ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        }
        if ((a = e.getAttribute(Xml.BUILD_NORMAL)) != null) {
            _buildNormal = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.TO_TERMINAL)) != null) {
            _sendToTerminal = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.ALLOW_LOCAL_MOVES)) != null) {
            _allowLocalMoves = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.ALLOW_THROUGH_CARS)) != null) {
            _allowThroughCars = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.ALLOW_RETURN)) != null) {
            _allowCarsReturnStaging = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.SERVICE_ALL)) != null) {
            _serviceAllCarsWithFinalDestinations = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.BUILD_CONSIST)) != null) {
            _buildConsist = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.SEND_CUSTOM_STAGING)) != null) {
            _sendCarsWithCustomLoadsToStaging = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.BUILT)) != null) {
            _built = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.BUILD)) != null) {
            _build = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.BUILD_FAILED)) != null) {
            _buildFailed = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.BUILD_FAILED_MESSAGE)) != null) {
            _buildFailedMessage = a.getValue();
        }
        if ((a = e.getAttribute(Xml.PRINTED)) != null) {
            _printed = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.MODIFIED)) != null) {
            _modified = a.getValue().equals(Xml.TRUE);
        }
        if ((a = e.getAttribute(Xml.SWITCH_LIST_STATUS)) != null) {
            _switchListStatus = a.getValue();
        }
        if ((a = e.getAttribute(Xml.LEAD_ENGINE)) != null) {
            _leadEngineId = a.getValue();
        }
        if ((a = e.getAttribute(Xml.TERMINATION_DATE)) != null) {
            _statusTerminatedDate = a.getValue();
        }
        if ((a = e.getAttribute(Xml.REQUESTED_CARS)) != null) {
            try {
                _statusCarsRequested = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Status cars requested ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        }
        if ((a = e.getAttribute(Xml.STATUS_CODE)) != null) {
            try {
                _statusCode = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Status code ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        } else if ((a = e.getAttribute(Xml.STATUS)) != null) {
            // attempt to recover status code
            String status = a.getValue();
            if (status.startsWith(BUILD_FAILED)) {
                _statusCode = CODE_BUILD_FAILED;
            } else if (status.startsWith(BUILT)) {
                _statusCode = CODE_BUILT;
            } else if (status.startsWith(PARTIAL_BUILT)) {
                _statusCode = CODE_PARTIAL_BUILT;
            } else if (status.startsWith(TERMINATED)) {
                String[] splitStatus = status.split(" ");
                if (splitStatus.length > 1) {
                    _statusTerminatedDate = splitStatus[1];
                }
                _statusCode = CODE_TERMINATED;
            } else if (status.startsWith(TRAIN_EN_ROUTE)) {
                _statusCode = CODE_TRAIN_EN_ROUTE;
            } else if (status.startsWith(TRAIN_RESET)) {
                _statusCode = CODE_TRAIN_RESET;
            } else {
                _statusCode = CODE_UNKNOWN;
            }
        }
        if ((a = e.getAttribute(Xml.OLD_STATUS_CODE)) != null) {
            try {
                _oldStatusCode = Integer.parseInt(a.getValue());
            } catch (NumberFormatException ee) {
                log.error("Old status code ({}) isn't a valid number for train ({})", a.getValue(), getName());
            }
        } else {
            _oldStatusCode = getStatusCode(); // use current status code if one
                                              // wasn't saved
        }
        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
            _comment = a.getValue();
        }
        if (getRoute() != null) {
            if ((a = e.getAttribute(Xml.CURRENT)) != null) {
                _current = getRoute().getLocationById(a.getValue());
            }
            if ((a = e.getAttribute(Xml.LEG2_START)) != null) {
                _leg2Start = getRoute().getLocationById(a.getValue());
            }
            if ((a = e.getAttribute(Xml.LEG3_START)) != null) {
                _leg3Start = getRoute().getLocationById(a.getValue());
            }
            if ((a = e.getAttribute(Xml.LEG2_END)) != null) {
                _end2Leg = getRoute().getLocationById(a.getValue());
            }
            if ((a = e.getAttribute(Xml.LEG3_END)) != null) {
                _leg3End = getRoute().getLocationById(a.getValue());
            }
            if ((a = e.getAttribute(Xml.DEPARTURE_TRACK)) != null) {
                Location location = InstanceManager.getDefault(LocationManager.class)
                        .getLocationByName(getTrainDepartsName());
                if (location != null) {
                    _departureTrack = location.getTrackById(a.getValue());
                } else {
                    log.error("Departure location not found for track {}", a.getValue());
                }
            }
            if ((a = e.getAttribute(Xml.TERMINATION_TRACK)) != null) {
                Location location = InstanceManager.getDefault(LocationManager.class)
                        .getLocationByName(getTrainTerminatesName());
                if (location != null) {
                    _terminationTrack = location.getTrackById(a.getValue());
                } else {
                    log.error("Termiation location not found for track {}", a.getValue());
                }
            }
        }

        // check for scripts
        if (e.getChild(Xml.SCRIPTS) != null) {
            List<Element> lb = e.getChild(Xml.SCRIPTS).getChildren(Xml.BUILD);
            for (Element es : lb) {
                if ((a = es.getAttribute(Xml.NAME)) != null) {
                    addBuildScript(a.getValue());
                }
            }
            List<Element> lab = e.getChild(Xml.SCRIPTS).getChildren(Xml.AFTER_BUILD);
            for (Element es : lab) {
                if ((a = es.getAttribute(Xml.NAME)) != null) {
                    addAfterBuildScript(a.getValue());
                }
            }
            List<Element> lm = e.getChild(Xml.SCRIPTS).getChildren(Xml.MOVE);
            for (Element es : lm) {
                if ((a = es.getAttribute(Xml.NAME)) != null) {
                    addMoveScript(a.getValue());
                }
            }
            List<Element> lt = e.getChild(Xml.SCRIPTS).getChildren(Xml.TERMINATE);
            for (Element es : lt) {
                if ((a = es.getAttribute(Xml.NAME)) != null) {
                    addTerminationScript(a.getValue());
                }
            }
        }
        // check for optional railroad name and logo
        if ((e.getChild(Xml.RAIL_ROAD) != null) && (a = e.getChild(Xml.RAIL_ROAD).getAttribute(Xml.NAME)) != null) {
            String name = a.getValue();
            setRailroadName(name);
        }
        if ((e.getChild(Xml.MANIFEST_LOGO) != null)) {
            if ((a = e.getChild(Xml.MANIFEST_LOGO).getAttribute(Xml.NAME)) != null) {
                setManifestLogoPathName(a.getValue());
            }
        }
        if ((a = e.getAttribute(Xml.SHOW_TIMES)) != null) {
            _showTimes = a.getValue().equals(Xml.TRUE);
        }

        addPropertyChangeListerners();
    }

    private void addPropertyChangeListerners() {
        InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
        InstanceManager.getDefault(EngineTypes.class).addPropertyChangeListener(this);
        InstanceManager.getDefault(CarOwners.class).addPropertyChangeListener(this);
        InstanceManager.getDefault(EngineModels.class).addPropertyChangeListener(this);
    }

    /**
     * Create an XML element to represent this Entry. This member has to remain
     * synchronized with the detailed DTD in operations-trains.dtd.
     *
     * @return Contents in a JDOM Element
     */
    public Element store() {
        Element e = new Element(Xml.TRAIN);
        e.setAttribute(Xml.ID, getId());
        e.setAttribute(Xml.NAME, getName());
        e.setAttribute(Xml.DESCRIPTION, getRawDescription());
        e.setAttribute(Xml.DEPART_HOUR, getDepartureTimeHour());
        e.setAttribute(Xml.DEPART_MINUTE, getDepartureTimeMinute());

        Element eRowColor = new Element(Xml.ROW_COLOR);
        eRowColor.setAttribute(Xml.NAME, getTableRowColorName());
        eRowColor.setAttribute(Xml.RESET_ROW_COLOR, getTableRowColorNameReset());
        e.addContent(eRowColor);

        Element eRoute = new Element(Xml.ROUTE);
        if (getRoute() != null) {
            eRoute.setAttribute(Xml.NAME, getRoute().getName());
            eRoute.setAttribute(Xml.ID, getRoute().getId());
            e.addContent(eRoute);
            // build list of locations that this train skips
            String[] locationIds = getTrainSkipsLocations();
            if (locationIds.length > 0) {
                Element eSkips = new Element(Xml.SKIPS);
                for (String id : locationIds) {
                    Element eLoc = new Element(Xml.LOCATION);
                    RouteLocation rl = getRoute().getLocationById(id);
                    if (rl != null) {
                        eLoc.setAttribute(Xml.NAME, rl.getName());
                        eLoc.setAttribute(Xml.ID, id);
                        eSkips.addContent(eLoc);
                    }
                }
                eRoute.addContent(eSkips);
            }
        }
        // build list of locations that this train skips
        if (getCurrentRouteLocation() != null) {
            e.setAttribute(Xml.CURRENT, getCurrentRouteLocation().getId());
        }
        if (getDepartureTrack() != null) {
            e.setAttribute(Xml.DEPARTURE_TRACK, getDepartureTrack().getId());
        }
        if (getTerminationTrack() != null) {
            e.setAttribute(Xml.TERMINATION_TRACK, getTerminationTrack().getId());
        }
        e.setAttribute(Xml.BUILT_START_YEAR, getBuiltStartYear());
        e.setAttribute(Xml.BUILT_END_YEAR, getBuiltEndYear());
        e.setAttribute(Xml.NUMBER_ENGINES, getNumberEngines());
        e.setAttribute(Xml.ENGINE_ROAD, getEngineRoad());
        e.setAttribute(Xml.ENGINE_MODEL, getEngineModel());
        e.setAttribute(Xml.REQUIRES, Integer.toString(getRequirements()));
        e.setAttribute(Xml.CABOOSE_ROAD, getCabooseRoad());
        e.setAttribute(Xml.BUILD_NORMAL, isBuildTrainNormalEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.TO_TERMINAL, isSendCarsToTerminalEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.ALLOW_LOCAL_MOVES, isAllowLocalMovesEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.ALLOW_RETURN, isAllowReturnToStagingEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.ALLOW_THROUGH_CARS, isAllowThroughCarsEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.SERVICE_ALL, isServiceAllCarsWithFinalDestinationsEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.SEND_CUSTOM_STAGING, isSendCarsWithCustomLoadsToStagingEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.BUILD_CONSIST, isBuildConsistEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.BUILT, isBuilt() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.BUILD, isBuildEnabled() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.BUILD_FAILED, isBuildFailed() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.BUILD_FAILED_MESSAGE, getBuildFailedMessage());
        e.setAttribute(Xml.PRINTED, isPrinted() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.MODIFIED, isModified() ? Xml.TRUE : Xml.FALSE);
        e.setAttribute(Xml.SWITCH_LIST_STATUS, getSwitchListStatus());
        if (getLeadEngine() != null) {
            e.setAttribute(Xml.LEAD_ENGINE, getLeadEngine().getId());
        }
        e.setAttribute(Xml.STATUS, getStatus());
        e.setAttribute(Xml.TERMINATION_DATE, getTerminationDate());
        e.setAttribute(Xml.REQUESTED_CARS, Integer.toString(getNumberCarsRequested()));
        e.setAttribute(Xml.STATUS_CODE, Integer.toString(getStatusCode()));
        e.setAttribute(Xml.OLD_STATUS_CODE, Integer.toString(getOldStatusCode()));
        e.setAttribute(Xml.COMMENT, getCommentWithColor());
        e.setAttribute(Xml.SHOW_TIMES, isShowArrivalAndDepartureTimesEnabled() ? Xml.TRUE : Xml.FALSE);
        // build list of car types for this train
        String[] types = getTypeNames();
        // new way of saving car types
        Element eTypes = new Element(Xml.TYPES);
        for (String type : types) {
            // don't save types that have been deleted by user
            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
                Element eType = new Element(Xml.LOCO_TYPE);
                eType.setAttribute(Xml.NAME, type);
                eTypes.addContent(eType);
            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
                Element eType = new Element(Xml.CAR_TYPE);
                eType.setAttribute(Xml.NAME, type);
                eTypes.addContent(eType);
            }
        }
        e.addContent(eTypes);
        // save list of car roads for this train
        if (!getCarRoadOption().equals(ALL_ROADS)) {
            e.setAttribute(Xml.CAR_ROAD_OPTION, getCarRoadOption());
            String[] roads = getCarRoadNames();
            // new way of saving road names
            Element eRoads = new Element(Xml.CAR_ROADS);
            for (String road : roads) {
                Element eRoad = new Element(Xml.CAR_ROAD);
                eRoad.setAttribute(Xml.NAME, road);
                eRoads.addContent(eRoad);
            }
            e.addContent(eRoads);
        }
        // save list of engine roads for this train
        if (!getLocoRoadOption().equals(ALL_ROADS)) {
            e.setAttribute(Xml.LOCO_ROAD_OPTION, getLocoRoadOption());
            String[] roads = getLocoRoadNames();
            Element eRoads = new Element(Xml.LOCO_ROADS);
            for (String road : roads) {
                Element eRoad = new Element(Xml.LOCO_ROAD);
                eRoad.setAttribute(Xml.NAME, road);
                eRoads.addContent(eRoad);
            }
            e.addContent(eRoads);
        }
        // save list of car loads for this train
        if (!getLoadOption().equals(ALL_LOADS)) {
            e.setAttribute(Xml.CAR_LOAD_OPTION, getLoadOption());
            String[] loads = getLoadNames();
            // new way of saving car loads
            Element eLoads = new Element(Xml.CAR_LOADS);
            for (String load : loads) {
                Element eLoad = new Element(Xml.CAR_LOAD);
                eLoad.setAttribute(Xml.NAME, load);
                eLoads.addContent(eLoad);
            }
            e.addContent(eLoads);
        }
        // save list of car owners for this train
        if (!getOwnerOption().equals(ALL_OWNERS)) {
            e.setAttribute(Xml.CAR_OWNER_OPTION, getOwnerOption());
            String[] owners = getOwnerNames();
            // new way of saving car owners
            Element eOwners = new Element(Xml.CAR_OWNERS);
            for (String owner : owners) {
                Element eOwner = new Element(Xml.CAR_OWNER);
                eOwner.setAttribute(Xml.NAME, owner);
                eOwners.addContent(eOwner);
            }
            e.addContent(eOwners);
        }
        // save list of scripts for this train
        if (getBuildScripts().size() > 0 ||
                getAfterBuildScripts().size() > 0 ||
                getMoveScripts().size() > 0 ||
                getTerminationScripts().size() > 0) {
            Element es = new Element(Xml.SCRIPTS);
            if (getBuildScripts().size() > 0) {
                for (String scriptPathname : getBuildScripts()) {
                    Element em = new Element(Xml.BUILD);
                    em.setAttribute(Xml.NAME, scriptPathname);
                    es.addContent(em);
                }
            }
            if (getAfterBuildScripts().size() > 0) {
                for (String scriptPathname : getAfterBuildScripts()) {
                    Element em = new Element(Xml.AFTER_BUILD);
                    em.setAttribute(Xml.NAME, scriptPathname);
                    es.addContent(em);
                }
            }
            if (getMoveScripts().size() > 0) {
                for (String scriptPathname : getMoveScripts()) {
                    Element em = new Element(Xml.MOVE);
                    em.setAttribute(Xml.NAME, scriptPathname);
                    es.addContent(em);
                }
            }
            // save list of termination scripts for this train
            if (getTerminationScripts().size() > 0) {
                for (String scriptPathname : getTerminationScripts()) {
                    Element et = new Element(Xml.TERMINATE);
                    et.setAttribute(Xml.NAME, scriptPathname);
                    es.addContent(et);
                }
            }
            e.addContent(es);
        }
        if (!getRailroadName().equals(NONE)) {
            Element r = new Element(Xml.RAIL_ROAD);
            r.setAttribute(Xml.NAME, getRailroadName());
            e.addContent(r);
        }
        if (!getManifestLogoPathName().equals(NONE)) {
            Element l = new Element(Xml.MANIFEST_LOGO);
            l.setAttribute(Xml.NAME, getManifestLogoPathName());
            e.addContent(l);
        }

        if (getSecondLegOptions() != NO_CABOOSE_OR_FRED) {
            e.setAttribute(Xml.LEG2_OPTIONS, Integer.toString(getSecondLegOptions()));
            e.setAttribute(Xml.LEG2_ENGINES, getSecondLegNumberEngines());
            e.setAttribute(Xml.LEG2_ROAD, getSecondLegEngineRoad());
            e.setAttribute(Xml.LEG2_MODEL, getSecondLegEngineModel());
            e.setAttribute(Xml.LEG2_CABOOSE_ROAD, getSecondLegCabooseRoad());
            if (getSecondLegStartRouteLocation() != null) {
                e.setAttribute(Xml.LEG2_START, getSecondLegStartRouteLocation().getId());
            }
            if (getSecondLegEndRouteLocation() != null) {
                e.setAttribute(Xml.LEG2_END, getSecondLegEndRouteLocation().getId());
            }
        }
        if (getThirdLegOptions() != NO_CABOOSE_OR_FRED) {
            e.setAttribute(Xml.LEG3_OPTIONS, Integer.toString(getThirdLegOptions()));
            e.setAttribute(Xml.LEG3_ENGINES, getThirdLegNumberEngines());
            e.setAttribute(Xml.LEG3_ROAD, getThirdLegEngineRoad());
            e.setAttribute(Xml.LEG3_MODEL, getThirdLegEngineModel());
            e.setAttribute(Xml.LEG3_CABOOSE_ROAD, getThirdLegCabooseRoad());
            if (getThirdLegStartRouteLocation() != null) {
                e.setAttribute(Xml.LEG3_START, getThirdLegStartRouteLocation().getId());
            }
            if (getThirdLegEndRouteLocation() != null) {
                e.setAttribute(Xml.LEG3_END, getThirdLegEndRouteLocation().getId());
            }
        }
        return e;
    }

    @Override
    public void propertyChange(java.beans.PropertyChangeEvent e) {
        if (Control.SHOW_PROPERTY) {
            log.debug("Train ({}) sees property change: ({}) old: ({}) new: ({})", getName(), e.getPropertyName(),
                    e.getOldValue(), e.getNewValue());
        }
        if (e.getPropertyName().equals(Route.DISPOSE)) {
            setRoute(null);
        }
        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY) ||
                e.getPropertyName().equals(CarTypes.CARTYPES_CHANGED_PROPERTY) ||
                e.getPropertyName().equals(EngineTypes.ENGINETYPES_NAME_CHANGED_PROPERTY)) {
            replaceType((String) e.getOldValue(), (String) e.getNewValue());
        }
        if (e.getPropertyName().equals(CarRoads.CARROADS_NAME_CHANGED_PROPERTY)) {
            replaceRoad((String) e.getOldValue(), (String) e.getNewValue());
        }
        if (e.getPropertyName().equals(CarOwners.CAROWNERS_NAME_CHANGED_PROPERTY)) {
            replaceOwner((String) e.getOldValue(), (String) e.getNewValue());
        }
        if (e.getPropertyName().equals(EngineModels.ENGINEMODELS_NAME_CHANGED_PROPERTY)) {
            replaceModel((String) e.getOldValue(), (String) e.getNewValue());
        }
        // forward route departure time property changes
        if (e.getPropertyName().equals(RouteLocation.DEPARTURE_TIME_CHANGED_PROPERTY)) {
            setDirtyAndFirePropertyChange(DEPARTURETIME_CHANGED_PROPERTY, e.getOldValue(), e.getNewValue());
        }
        // forward any property changes in this train's route
        if (e.getSource().getClass().equals(Route.class)) {
            setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
        }
    }

    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
        InstanceManager.getDefault(TrainManagerXml.class).setDirty(true);
        firePropertyChange(p, old, n);
    }

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Train.class);

}