001package jmri.jmrit.operations.locations;
002
003import java.util.*;
004
005import org.jdom2.Attribute;
006import org.jdom2.Element;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import jmri.InstanceManager;
011import jmri.Reporter;
012import jmri.beans.PropertyChangeSupport;
013import jmri.jmrit.operations.locations.divisions.Division;
014import jmri.jmrit.operations.locations.schedules.*;
015import jmri.jmrit.operations.rollingstock.RollingStock;
016import jmri.jmrit.operations.rollingstock.cars.*;
017import jmri.jmrit.operations.rollingstock.engines.Engine;
018import jmri.jmrit.operations.rollingstock.engines.EngineTypes;
019import jmri.jmrit.operations.routes.Route;
020import jmri.jmrit.operations.routes.RouteLocation;
021import jmri.jmrit.operations.setup.Setup;
022import jmri.jmrit.operations.trains.Train;
023import jmri.jmrit.operations.trains.TrainManager;
024import jmri.jmrit.operations.trains.schedules.TrainSchedule;
025import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
026import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
027
028/**
029 * Represents a location (track) on the layout Can be a spur, yard, staging, or
030 * interchange track.
031 *
032 * @author Daniel Boudreau Copyright (C) 2008 - 2014
033 */
034public class Track extends PropertyChangeSupport {
035
036    public static final String NONE = "";
037
038    protected String _id = NONE;
039    protected String _name = NONE;
040    protected String _trackType = NONE; // yard, spur, interchange or staging
041    protected Location _location; // the location for this track
042    protected int _trainDir = EAST + WEST + NORTH + SOUTH; // train directions
043    protected int _numberRS = 0; // number of cars and engines
044    protected int _numberCars = 0; // number of cars
045    protected int _numberEngines = 0; // number of engines
046    protected int _pickupRS = 0; // number of pick ups by trains
047    protected int _dropRS = 0; // number of set outs by trains
048    protected int _length = 0; // length of track
049    protected int _reserved = 0; // length of track reserved by trains
050    protected int _reservedLengthSetouts = 0; // reserved for car drops
051    protected int _reservedLengthPickups = 0; // reserved for car pulls
052    protected int _numberCarsEnRoute = 0; // number of cars en-route
053    protected int _usedLength = 0; // length of track filled by cars and engines
054    protected int _ignoreUsedLengthPercentage = IGNORE_0;
055    // ignore values 0 - 100%
056    public static final int IGNORE_0 = 0;
057    public static final int IGNORE_25 = 25;
058    public static final int IGNORE_50 = 50;
059    public static final int IGNORE_75 = 75;
060    public static final int IGNORE_100 = 100;
061    protected int _moves = 0; // count of the drops since creation
062    protected int _blockingOrder = 0; // the order tracks are serviced
063    protected String _alternateTrackId = NONE; // the alternate track id
064    protected String _comment = NONE;
065
066    // car types serviced by this track
067    protected List<String> _typeList = new ArrayList<>();
068
069    // Manifest and switch list comments
070    protected boolean _printCommentManifest = true;
071    protected boolean _printCommentSwitchList = false;
072    protected String _commentPickup = NONE;
073    protected String _commentSetout = NONE;
074    protected String _commentBoth = NONE;
075
076    // road options
077    protected String _roadOption = ALL_ROADS; // controls car roads
078    protected List<String> _roadList = new ArrayList<>();
079
080    // load options
081    protected String _loadOption = ALL_LOADS; // receive track load restrictions
082    protected List<String> _loadList = new ArrayList<>();
083    protected String _shipLoadOption = ALL_LOADS;// ship track load restrictions
084    protected List<String> _shipLoadList = new ArrayList<>();
085
086    // destinations that this track will service
087    protected String _destinationOption = ALL_DESTINATIONS;
088    protected List<String> _destinationIdList = new ArrayList<>();
089
090    // schedule options
091    protected String _scheduleName = NONE; // Schedule name if there's one
092    protected String _scheduleId = NONE; // Schedule id if there's one
093    protected String _scheduleItemId = NONE; // the current scheduled item id
094    protected int _scheduleCount = 0; // item count
095    protected int _reservedEnRoute = 0; // length of cars en-route to this track
096    protected int _reservationFactor = 100; // percentage of track space for
097                                            // cars en-route
098    protected int _mode = MATCH; // default is match mode
099    protected boolean _holdCustomLoads = false; // hold cars with custom loads
100
101    // drop & pick up options
102    protected String _dropOption = ANY; // controls which route or train can set
103                                        // out cars
104    protected String _pickupOption = ANY; // controls which route or train can
105                                          // pick up cars
106    public static final String ANY = "Any"; // track accepts any train or route
107    public static final String TRAINS = "trains"; // track accepts trains
108    public static final String ROUTES = "routes"; // track accepts routes
109    public static final String EXCLUDE_TRAINS = "excludeTrains";
110    public static final String EXCLUDE_ROUTES = "excludeRoutes";
111    protected List<String> _dropList = new ArrayList<>();
112    protected List<String> _pickupList = new ArrayList<>();
113
114    // load options for staging
115    protected int _loadOptions = 0;
116    private static final int SWAP_GENERIC_LOADS = 1;
117    private static final int EMPTY_CUSTOM_LOADS = 2;
118    private static final int GENERATE_CUSTOM_LOADS = 4;
119    private static final int GENERATE_CUSTOM_LOADS_ANY_SPUR = 8;
120    private static final int EMPTY_GENERIC_LOADS = 16;
121    private static final int GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK = 32;
122
123    // load options for spur
124    private static final int DISABLE_LOAD_CHANGE = 64;
125    private static final int QUICK_SERVICE = 128;
126
127    // block options
128    protected int _blockOptions = 0;
129    private static final int BLOCK_CARS = 1;
130
131    // order cars are serviced
132    protected String _order = NORMAL;
133    public static final String NORMAL = Bundle.getMessage("Normal");
134    public static final String FIFO = Bundle.getMessage("FIFO");
135    public static final String LIFO = Bundle.getMessage("LIFO");
136
137    // Priority
138    protected String _trackPriority = PRIORITY_NORMAL;
139    public static final String PRIORITY_HIGH = Bundle.getMessage("High");
140    public static final String PRIORITY_MEDIUM = Bundle.getMessage("Medium");
141    public static final String PRIORITY_NORMAL = Bundle.getMessage("Normal");
142    public static final String PRIORITY_LOW = Bundle.getMessage("Low");
143
144    // the four types of tracks
145    public static final String STAGING = "Staging";
146    public static final String INTERCHANGE = "Interchange";
147    public static final String YARD = "Yard";
148    // note that code before 2020 (4.21.1) used Siding as the spur type
149    public static final String SPUR = "Spur"; 
150    private static final String SIDING = "Siding"; // For loading older files
151
152    // train directions serviced by this track
153    public static final int EAST = 1;
154    public static final int WEST = 2;
155    public static final int NORTH = 4;
156    public static final int SOUTH = 8;
157
158    // how roads are serviced by this track
159    public static final String ALL_ROADS = Bundle.getMessage("All"); 
160    // track accepts only certain roads
161    public static final String INCLUDE_ROADS = Bundle.getMessage("Include");
162    // track excludes certain roads
163    public static final String EXCLUDE_ROADS = Bundle.getMessage("Exclude");
164
165    // load options
166    public static final String ALL_LOADS = Bundle.getMessage("All"); 
167    public static final String INCLUDE_LOADS = Bundle.getMessage("Include");
168    public static final String EXCLUDE_LOADS = Bundle.getMessage("Exclude");
169
170    // destination options
171    public static final String ALL_DESTINATIONS = Bundle.getMessage("All"); 
172    public static final String INCLUDE_DESTINATIONS = Bundle.getMessage("Include");
173    public static final String EXCLUDE_DESTINATIONS = Bundle.getMessage("Exclude");
174    // when true only cars with final destinations are allowed to use track
175    protected boolean _onlyCarsWithFD = false;
176
177    // schedule modes
178    public static final int SEQUENTIAL = 0;
179    public static final int MATCH = 1;
180
181    // pickup status
182    public static final String PICKUP_OKAY = "";
183
184    // pool
185    protected Pool _pool = null;
186    protected int _minimumLength = 0;
187    protected int _maximumLength = Integer.MAX_VALUE;
188
189    // return status when checking rolling stock
190    public static final String OKAY = Bundle.getMessage("okay");
191    public static final String LENGTH = Bundle.getMessage("rollingStock") +
192            " " +
193            Bundle.getMessage("Length").toLowerCase(); // lower case in report
194    public static final String TYPE = Bundle.getMessage("type");
195    public static final String ROAD = Bundle.getMessage("road");
196    public static final String LOAD = Bundle.getMessage("load");
197    public static final String CAPACITY = Bundle.getMessage("track") + " " + Bundle.getMessage("capacity");
198    public static final String SCHEDULE = Bundle.getMessage("schedule");
199    public static final String CUSTOM = Bundle.getMessage("custom");
200    public static final String DESTINATION = Bundle.getMessage("carDestination");
201    public static final String NO_FINAL_DESTINATION = Bundle.getMessage("noFinalDestination");
202
203    // For property change
204    public static final String TYPES_CHANGED_PROPERTY = "trackRollingStockTypes"; // NOI18N
205    public static final String ROADS_CHANGED_PROPERTY = "trackRoads"; // NOI18N
206    public static final String NAME_CHANGED_PROPERTY = "trackName"; // NOI18N
207    public static final String LENGTH_CHANGED_PROPERTY = "trackLength"; // NOI18N
208    public static final String MIN_LENGTH_CHANGED_PROPERTY = "trackMinLength"; // NOI18N
209    public static final String MAX_LENGTH_CHANGED_PROPERTY = "trackMaxLength"; // NOI18N
210    public static final String SCHEDULE_CHANGED_PROPERTY = "trackScheduleChange"; // NOI18N
211    public static final String DISPOSE_CHANGED_PROPERTY = "trackDispose"; // NOI18N
212    public static final String TRAIN_DIRECTION_CHANGED_PROPERTY = "trackTrainDirection"; // NOI18N
213    public static final String DROP_CHANGED_PROPERTY = "trackDrop"; // NOI18N
214    public static final String PICKUP_CHANGED_PROPERTY = "trackPickup"; // NOI18N
215    public static final String TRACK_TYPE_CHANGED_PROPERTY = "trackType"; // NOI18N
216    public static final String LOADS_CHANGED_PROPERTY = "trackLoads"; // NOI18N
217    public static final String POOL_CHANGED_PROPERTY = "trackPool"; // NOI18N
218    public static final String PLANNED_PICKUPS_CHANGED_PROPERTY = "plannedPickUps"; // NOI18N
219    public static final String LOAD_OPTIONS_CHANGED_PROPERTY = "trackLoadOptions"; // NOI18N
220    public static final String DESTINATIONS_CHANGED_PROPERTY = "trackDestinations"; // NOI18N
221    public static final String DESTINATION_OPTIONS_CHANGED_PROPERTY = "trackDestinationOptions"; // NOI18N
222    public static final String SCHEDULE_MODE_CHANGED_PROPERTY = "trackScheduleMode"; // NOI18N
223    public static final String SCHEDULE_ID_CHANGED_PROPERTY = "trackScheduleId"; // NOI18N
224    public static final String SERVICE_ORDER_CHANGED_PROPERTY = "trackServiceOrder"; // NOI18N
225    public static final String ALTERNATE_TRACK_CHANGED_PROPERTY = "trackAlternate"; // NOI18N
226    public static final String TRACK_BLOCKING_ORDER_CHANGED_PROPERTY = "trackBlockingOrder"; // NOI18N
227    public static final String TRACK_REPORTER_CHANGED_PROPERTY = "trackReporterChange"; // NOI18N
228    public static final String ROUTED_CHANGED_PROPERTY = "onlyCarsWithFinalDestinations"; // NOI18N
229    public static final String HOLD_CARS_CHANGED_PROPERTY = "trackHoldCarsWithCustomLoads"; // NOI18N
230    public static final String TRACK_COMMENT_CHANGED_PROPERTY = "trackComments"; // NOI18N
231    public static final String TRACK_FACTOR_CHANGED_PROPERTY = "trackReservationFactor"; // NOI18N
232    public static final String PRIORITY_CHANGED_PROPERTY = "trackPriority"; // NOI18N
233
234    // IdTag reader associated with this track.
235    protected Reporter _reader = null;
236
237    public Track(String id, String name, String type, Location location) {
238        log.debug("New ({}) track ({}) id: {}", type, name, id);
239        _location = location;
240        _trackType = type;
241        _name = name;
242        _id = id;
243        // a new track accepts all types
244        setTypeNames(InstanceManager.getDefault(CarTypes.class).getNames());
245        setTypeNames(InstanceManager.getDefault(EngineTypes.class).getNames());
246    }
247
248    /**
249     * Creates a copy of this track.
250     *
251     * @param newName     The name of the new track.
252     * @param newLocation The location of the new track.
253     * @return Track
254     */
255    public Track copyTrack(String newName, Location newLocation) {
256        Track newTrack = newLocation.addTrack(newName, getTrackType());
257        newTrack.clearTypeNames(); // all types are accepted by a new track
258
259        newTrack.setAddCustomLoadsAnySpurEnabled(isAddCustomLoadsAnySpurEnabled());
260        newTrack.setAddCustomLoadsAnyStagingTrackEnabled(isAddCustomLoadsAnyStagingTrackEnabled());
261        newTrack.setAddCustomLoadsEnabled(isAddCustomLoadsEnabled());
262
263        newTrack.setAlternateTrack(getAlternateTrack());
264        newTrack.setBlockCarsEnabled(isBlockCarsEnabled());
265        newTrack.setComment(getComment());
266        newTrack.setCommentBoth(getCommentBothWithColor());
267        newTrack.setCommentPickup(getCommentPickupWithColor());
268        newTrack.setCommentSetout(getCommentSetoutWithColor());
269
270        newTrack.setDestinationOption(getDestinationOption());
271        newTrack.setDestinationIds(getDestinationIds());
272
273        // must set option before setting ids
274        newTrack.setDropOption(getDropOption()); 
275        newTrack.setDropIds(getDropIds());
276
277        newTrack.setIgnoreUsedLengthPercentage(getIgnoreUsedLengthPercentage());
278        newTrack.setLength(getLength());
279        newTrack.setLoadEmptyEnabled(isLoadEmptyEnabled());
280        newTrack.setLoadNames(getLoadNames());
281        newTrack.setLoadOption(getLoadOption());
282        newTrack.setLoadSwapEnabled(isLoadSwapEnabled());
283
284        newTrack.setOnlyCarsWithFinalDestinationEnabled(isOnlyCarsWithFinalDestinationEnabled());
285
286        // must set option before setting ids
287        newTrack.setPickupOption(getPickupOption());
288        newTrack.setPickupIds(getPickupIds());
289
290        // track pools are only shared within a specific location
291        if (getPool() != null) {
292            newTrack.setPool(newLocation.addPool(getPool().getName()));
293            newTrack.setPoolMinimumLength(getPoolMinimumLength());
294            newTrack.setPoolMaximumLength(getPoolMaximumLength());
295        }
296
297        newTrack.setPrintManifestCommentEnabled(isPrintManifestCommentEnabled());
298        newTrack.setPrintSwitchListCommentEnabled(isPrintSwitchListCommentEnabled());
299
300        newTrack.setRemoveCustomLoadsEnabled(isRemoveCustomLoadsEnabled());
301        newTrack.setReservationFactor(getReservationFactor());
302        newTrack.setRoadNames(getRoadNames());
303        newTrack.setRoadOption(getRoadOption());
304        newTrack.setSchedule(getSchedule());
305        newTrack.setScheduleMode(getScheduleMode());
306        newTrack.setServiceOrder(getServiceOrder());
307        newTrack.setShipLoadNames(getShipLoadNames());
308        newTrack.setShipLoadOption(getShipLoadOption());
309        newTrack.setTrainDirections(getTrainDirections());
310        newTrack.setTypeNames(getTypeNames());
311
312        newTrack.setDisableLoadChangeEnabled(isDisableLoadChangeEnabled());
313        newTrack.setQuickServiceEnabled(isQuickServiceEnabled());
314        newTrack.setHoldCarsWithCustomLoadsEnabled(isHoldCarsWithCustomLoadsEnabled());
315        newTrack.setTrackPriority(getTrackPriority());
316        return newTrack;
317    }
318
319    // for combo boxes
320    @Override
321    public String toString() {
322        return _name;
323    }
324
325    public String getId() {
326        return _id;
327    }
328
329    public Location getLocation() {
330        return _location;
331    }
332
333    public void setName(String name) {
334        String old = _name;
335        _name = name;
336        if (!old.equals(name)) {
337            // recalculate max track name length
338            InstanceManager.getDefault(LocationManager.class).resetNameLengths();
339            setDirtyAndFirePropertyChange(NAME_CHANGED_PROPERTY, old, name);
340        }
341    }
342
343    public String getName() {
344        return _name;
345    }
346    
347    public String getSplitName() {
348        return TrainCommon.splitString(getName());
349    }
350
351    public Division getDivision() {
352        return getLocation().getDivision();
353    }
354
355    public String getDivisionName() {
356        return getLocation().getDivisionName();
357    }
358
359    public boolean isSpur() {
360        return getTrackType().equals(Track.SPUR);
361    }
362
363    public boolean isYard() {
364        return getTrackType().equals(Track.YARD);
365    }
366
367    public boolean isInterchange() {
368        return getTrackType().equals(Track.INTERCHANGE);
369    }
370
371    public boolean isStaging() {
372        return getTrackType().equals(Track.STAGING);
373    }
374
375    public boolean hasMessages() {
376        if (!getCommentBoth().isBlank() ||
377                !getCommentPickup().isBlank() ||
378                !getCommentSetout().isBlank()) {
379            return true;
380        }
381        return false;
382    }
383
384    /**
385     * Gets the track type
386     *
387     * @return Track.SPUR Track.YARD Track.INTERCHANGE or Track.STAGING
388     */
389    public String getTrackType() {
390        return _trackType;
391    }
392
393    /**
394     * Sets the track type, spur, interchange, yard, staging
395     *
396     * @param type Track.SPUR Track.YARD Track.INTERCHANGE Track.STAGING
397     */
398    public void setTrackType(String type) {
399        String old = _trackType;
400        _trackType = type;
401        if (!old.equals(type)) {
402            setDirtyAndFirePropertyChange(TRACK_TYPE_CHANGED_PROPERTY, old, type);
403        }
404    }
405
406    public String getTrackTypeName() {
407        return (getTrackTypeName(getTrackType()));
408    }
409
410    public static String getTrackTypeName(String trackType) {
411        if (trackType.equals(Track.SPUR)) {
412            return Bundle.getMessage("Spur").toLowerCase();
413        }
414        if (trackType.equals(Track.YARD)) {
415            return Bundle.getMessage("Yard").toLowerCase();
416        }
417        if (trackType.equals(Track.INTERCHANGE)) {
418            return Bundle.getMessage("Class/Interchange"); // abbreviation
419        }
420        if (trackType.equals(Track.STAGING)) {
421            return Bundle.getMessage("Staging").toLowerCase();
422        }
423        return ("unknown"); // NOI18N
424    }
425
426    public void setLength(int length) {
427        int old = _length;
428        _length = length;
429        if (old != length) {
430            setDirtyAndFirePropertyChange(LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length));
431        }
432    }
433
434    public int getLength() {
435        return _length;
436    }
437
438    /**
439     * Sets the minimum length of this track when the track is in a pool.
440     *
441     * @param length minimum
442     */
443    public void setPoolMinimumLength(int length) {
444        int old = _minimumLength;
445        _minimumLength = length;
446        if (old != length) {
447            setDirtyAndFirePropertyChange(MIN_LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length));
448        }
449    }
450
451    public int getPoolMinimumLength() {
452        return _minimumLength;
453    }
454
455    /**
456     * Sets the maximum length of this track when the track is in a pool.
457     *
458     * @param length maximum
459     */
460    public void setPoolMaximumLength(int length) {
461        int old = _maximumLength;
462        _maximumLength = length;
463        if (old != length) {
464            setDirtyAndFirePropertyChange(MAX_LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length));
465        }
466    }
467
468    public int getPoolMaximumLength() {
469        return _maximumLength;
470    }
471
472    /**
473     * The amount of track space that is reserved for car drops or pick ups. Can
474     * be positive or negative.
475     * 
476     * @param reserved the calculated track space
477     */
478    protected void setReserved(int reserved) {
479        int old = _reserved;
480        _reserved = reserved;
481        if (old != reserved) {
482            setDirtyAndFirePropertyChange("trackReserved", Integer.toString(old), // NOI18N
483                    Integer.toString(reserved)); // NOI18N
484        }
485    }
486
487    public int getReserved() {
488        return _reserved;
489    }
490
491    public void addReservedInRoute(Car car) {
492        int old = _reservedEnRoute;
493        _numberCarsEnRoute++;
494        _reservedEnRoute = old + car.getTotalLength();
495        if (old != _reservedEnRoute) {
496            setDirtyAndFirePropertyChange("trackAddReservedInRoute", Integer.toString(old), // NOI18N
497                    Integer.toString(_reservedEnRoute)); // NOI18N
498        }
499    }
500
501    public void deleteReservedInRoute(Car car) {
502        int old = _reservedEnRoute;
503        _numberCarsEnRoute--;
504        _reservedEnRoute = old - car.getTotalLength();
505        if (old != _reservedEnRoute) {
506            setDirtyAndFirePropertyChange("trackDeleteReservedInRoute", Integer.toString(old), // NOI18N
507                    Integer.toString(_reservedEnRoute)); // NOI18N
508        }
509    }
510
511    /**
512     * Used to determine how much track space is going to be consumed by cars in
513     * route to this track. See isSpaceAvailable().
514     *
515     * @return The length of all cars en route to this track including couplers.
516     */
517    public int getReservedInRoute() {
518        return _reservedEnRoute;
519    }
520
521    public int getNumberOfCarsInRoute() {
522        return _numberCarsEnRoute;
523    }
524
525    /**
526     * Set the reservation factor. Default 100 (100%). Used by the program when
527     * generating car loads from staging. A factor of 100% allows the program to
528     * fill a track with car loads. Numbers over 100% can overload a track.
529     *
530     * @param factor A number from 0 to 10000.
531     */
532    public void setReservationFactor(int factor) {
533        int old = _reservationFactor;
534        _reservationFactor = factor;
535        if (old != factor) {
536            setDirtyAndFirePropertyChange(TRACK_FACTOR_CHANGED_PROPERTY, old, factor); // NOI18N
537        }
538    }
539
540    public int getReservationFactor() {
541        return _reservationFactor;
542    }
543
544    /**
545     * Sets the mode of operation for the schedule assigned to this track.
546     *
547     * @param mode Track.SEQUENTIAL or Track.MATCH
548     */
549    public void setScheduleMode(int mode) {
550        int old = _mode;
551        _mode = mode;
552        if (old != mode) {
553            setDirtyAndFirePropertyChange(SCHEDULE_MODE_CHANGED_PROPERTY, old, mode); // NOI18N
554        }
555    }
556
557    /**
558     * Gets the mode of operation for the schedule assigned to this track.
559     *
560     * @return Mode of operation: Track.SEQUENTIAL or Track.MATCH
561     */
562    public int getScheduleMode() {
563        return _mode;
564    }
565
566    public String getScheduleModeName() {
567        if (getScheduleMode() == Track.MATCH) {
568            return Bundle.getMessage("Match");
569        }
570        return Bundle.getMessage("Sequential");
571    }
572
573    public void setAlternateTrack(Track track) {
574        Track oldTrack = _location.getTrackById(_alternateTrackId);
575        String old = _alternateTrackId;
576        if (track != null) {
577            _alternateTrackId = track.getId();
578        } else {
579            _alternateTrackId = NONE;
580        }
581        if (!old.equals(_alternateTrackId)) {
582            setDirtyAndFirePropertyChange(ALTERNATE_TRACK_CHANGED_PROPERTY, oldTrack, track);
583        }
584    }
585
586    /**
587     * Returns the alternate track for a spur
588     * 
589     * @return alternate track
590     */
591    public Track getAlternateTrack() {
592        if (!isSpur()) {
593            return null;
594        }
595        return _location.getTrackById(_alternateTrackId);
596    }
597
598    public void setHoldCarsWithCustomLoadsEnabled(boolean enable) {
599        boolean old = _holdCustomLoads;
600        _holdCustomLoads = enable;
601        setDirtyAndFirePropertyChange(HOLD_CARS_CHANGED_PROPERTY, old, enable);
602    }
603
604    /**
605     * If enabled (true), hold cars with custom loads rather than allowing them
606     * to go to staging if the spur and the alternate track were full. If
607     * disabled, cars with custom loads can be forwarded to staging when this
608     * spur and all others with this option are also false.
609     * 
610     * @return True if enabled
611     */
612    public boolean isHoldCarsWithCustomLoadsEnabled() {
613        return _holdCustomLoads;
614    }
615
616    /**
617     * Used to determine if there's space available at this track for the car.
618     * Considers cars en-route to this track. Used to prevent overloading the
619     * track.
620     *
621     * @param car The car to be set out.
622     * @return true if space available.
623     */
624    public boolean isSpaceAvailable(Car car) {
625        int carLength = car.getTotalKernelLength();
626        int trackLength = getLength();
627        // is the car or kernel too long for the track?
628        if (trackLength < carLength && getPool() == null) {
629            return false;
630        }
631        // is track part of a pool?
632        if (getPool() != null && getPool().getMaxLengthTrack(this) < carLength) {
633            return false;
634        }
635        // ignore reservation factor unless car is departing staging
636        if (car.getTrack() != null && car.getTrack().isStaging()) {
637            return (getLength() * getReservationFactor() / 100 - (getReservedInRoute() + carLength) >= 0);
638        }
639        // if there's alternate, include that length in the calculation
640        if (getAlternateTrack() != null) {
641            trackLength = trackLength + getAlternateTrack().getLength();
642        }
643        return (trackLength - (getReservedInRoute() + carLength) >= 0);
644    }
645
646    public void setUsedLength(int length) {
647        int old = _usedLength;
648        _usedLength = length;
649        if (old != length) {
650            setDirtyAndFirePropertyChange("trackUsedLength", Integer.toString(old), // NOI18N
651                    Integer.toString(length));
652        }
653    }
654
655    public int getUsedLength() {
656        return _usedLength;
657    }
658
659    /**
660     * The amount of consumed track space to be ignored when sending new rolling
661     * stock to the track. See Planned Pickups in help.
662     *
663     * @param percentage a number between 0 and 100
664     */
665    public void setIgnoreUsedLengthPercentage(int percentage) {
666        int old = _ignoreUsedLengthPercentage;
667        _ignoreUsedLengthPercentage = percentage;
668        if (old != percentage) {
669            setDirtyAndFirePropertyChange(PLANNED_PICKUPS_CHANGED_PROPERTY, Integer.toString(old),
670                    Integer.toString(percentage));
671        }
672    }
673
674    public int getIgnoreUsedLengthPercentage() {
675        return _ignoreUsedLengthPercentage;
676    }
677
678    /**
679     * Sets the number of rolling stock (cars and or engines) on this track
680     */
681    private void setNumberRS(int number) {
682        int old = _numberRS;
683        _numberRS = number;
684        if (old != number) {
685            setDirtyAndFirePropertyChange("trackNumberRS", Integer.toString(old), // NOI18N
686                    Integer.toString(number)); // NOI18N
687        }
688    }
689
690    /**
691     * Sets the number of cars on this track
692     */
693    private void setNumberCars(int number) {
694        int old = _numberCars;
695        _numberCars = number;
696        if (old != number) {
697            setDirtyAndFirePropertyChange("trackNumberCars", Integer.toString(old), // NOI18N
698                    Integer.toString(number));
699        }
700    }
701
702    /**
703     * Sets the number of engines on this track
704     */
705    private void setNumberEngines(int number) {
706        int old = _numberEngines;
707        _numberEngines = number;
708        if (old != number) {
709            setDirtyAndFirePropertyChange("trackNumberEngines", Integer.toString(old), // NOI18N
710                    Integer.toString(number));
711        }
712    }
713
714    /**
715     * @return The number of rolling stock (cars and engines) on this track
716     */
717    public int getNumberRS() {
718        return _numberRS;
719    }
720
721    /**
722     * @return The number of cars on this track
723     */
724    public int getNumberCars() {
725        return _numberCars;
726    }
727
728    /**
729     * @return The number of engines on this track
730     */
731    public int getNumberEngines() {
732        return _numberEngines;
733    }
734
735    /**
736     * Adds rolling stock to a specific track.
737     * 
738     * @param rs The rolling stock to place on the track.
739     */
740    public void addRS(RollingStock rs) {
741        setNumberRS(getNumberRS() + 1);
742        if (rs.getClass() == Car.class) {
743            setNumberCars(getNumberCars() + 1);
744        } else if (rs.getClass() == Engine.class) {
745            setNumberEngines(getNumberEngines() + 1);
746        }
747        setUsedLength(getUsedLength() + rs.getTotalLength());
748    }
749
750    public void deleteRS(RollingStock rs) {
751        setNumberRS(getNumberRS() - 1);
752        if (rs.getClass() == Car.class) {
753            setNumberCars(getNumberCars() - 1);
754        } else if (rs.getClass() == Engine.class) {
755            setNumberEngines(getNumberEngines() - 1);
756        }
757        setUsedLength(getUsedLength() - rs.getTotalLength());
758    }
759
760    /**
761     * Increments the number of cars and or engines that will be picked up by a
762     * train from this track.
763     * 
764     * @param rs The rolling stock.
765     */
766    public void addPickupRS(RollingStock rs) {
767        int old = _pickupRS;
768        _pickupRS++;
769        if (Setup.isBuildAggressive()) {
770            setReserved(getReserved() - rs.getTotalLength());
771        }
772        _reservedLengthPickups = _reservedLengthPickups + rs.getTotalLength();
773        setDirtyAndFirePropertyChange("trackPickupRS", Integer.toString(old), // NOI18N
774                Integer.toString(_pickupRS));
775    }
776
777    public void deletePickupRS(RollingStock rs) {
778        int old = _pickupRS;
779        if (Setup.isBuildAggressive()) {
780            setReserved(getReserved() + rs.getTotalLength());
781        }
782        _reservedLengthPickups = _reservedLengthPickups - rs.getTotalLength();
783        _pickupRS--;
784        setDirtyAndFirePropertyChange("trackDeletePickupRS", Integer.toString(old), // NOI18N
785                Integer.toString(_pickupRS));
786    }
787
788    /**
789     * @return the number of rolling stock (cars and or locos) that are
790     *         scheduled for pick up from this track.
791     */
792    public int getPickupRS() {
793        return _pickupRS;
794    }
795
796    public int getReservedLengthPickups() {
797        return _reservedLengthPickups;
798    }
799
800    public void addDropRS(RollingStock rs) {
801        int old = _dropRS;
802        _dropRS++;
803        bumpMoves();
804        // don't reserve clones
805        if (rs.isClone()) {
806            log.debug("Ignoring clone {} add drop reserve", rs.toString());
807        } else {
808            setReserved(getReserved() + rs.getTotalLength());
809        }
810        _reservedLengthSetouts = _reservedLengthSetouts + rs.getTotalLength();
811        setDirtyAndFirePropertyChange("trackAddDropRS", Integer.toString(old), Integer.toString(_dropRS)); // NOI18N
812    }
813
814    public void deleteDropRS(RollingStock rs) {
815        int old = _dropRS;
816        _dropRS--;
817        // don't reserve clones
818        if (rs.isClone()) {
819            log.debug("Ignoring clone {} delete drop reserve", rs.toString());
820        } else {
821            setReserved(getReserved() - rs.getTotalLength());
822        }
823        _reservedLengthSetouts = _reservedLengthSetouts - rs.getTotalLength();
824        setDirtyAndFirePropertyChange("trackDeleteDropRS", Integer.toString(old), // NOI18N
825                Integer.toString(_dropRS));
826    }
827
828    public int getDropRS() {
829        return _dropRS;
830    }
831
832    public int getReservedLengthSetouts() {
833        return _reservedLengthSetouts;
834    }
835
836    public void setComment(String comment) {
837        String old = _comment;
838        _comment = comment;
839        if (!old.equals(comment)) {
840            setDirtyAndFirePropertyChange("trackComment", old, comment); // NOI18N
841        }
842    }
843
844    public String getComment() {
845        return _comment;
846    }
847
848    public void setCommentPickup(String comment) {
849        String old = _commentPickup;
850        _commentPickup = comment;
851        if (!old.equals(comment)) {
852            setDirtyAndFirePropertyChange(TRACK_COMMENT_CHANGED_PROPERTY, old, comment); // NOI18N
853        }
854    }
855
856    public String getCommentPickup() {
857        return TrainCommon.getTextColorString(getCommentPickupWithColor());
858    }
859
860    public String getCommentPickupWithColor() {
861        return _commentPickup;
862    }
863
864    public void setCommentSetout(String comment) {
865        String old = _commentSetout;
866        _commentSetout = comment;
867        if (!old.equals(comment)) {
868            setDirtyAndFirePropertyChange(TRACK_COMMENT_CHANGED_PROPERTY, old, comment); // NOI18N
869        }
870    }
871
872    public String getCommentSetout() {
873        return TrainCommon.getTextColorString(getCommentSetoutWithColor());
874    }
875
876    public String getCommentSetoutWithColor() {
877        return _commentSetout;
878    }
879
880    public void setCommentBoth(String comment) {
881        String old = _commentBoth;
882        _commentBoth = comment;
883        if (!old.equals(comment)) {
884            setDirtyAndFirePropertyChange(TRACK_COMMENT_CHANGED_PROPERTY, old, comment); // NOI18N
885        }
886    }
887
888    public String getCommentBoth() {
889        return TrainCommon.getTextColorString(getCommentBothWithColor());
890    }
891
892    public String getCommentBothWithColor() {
893        return _commentBoth;
894    }
895
896    public boolean isPrintManifestCommentEnabled() {
897        return _printCommentManifest;
898    }
899
900    public void setPrintManifestCommentEnabled(boolean enable) {
901        boolean old = isPrintManifestCommentEnabled();
902        _printCommentManifest = enable;
903        setDirtyAndFirePropertyChange("trackPrintManifestComment", old, enable);
904    }
905
906    public boolean isPrintSwitchListCommentEnabled() {
907        return _printCommentSwitchList;
908    }
909
910    public void setPrintSwitchListCommentEnabled(boolean enable) {
911        boolean old = isPrintSwitchListCommentEnabled();
912        _printCommentSwitchList = enable;
913        setDirtyAndFirePropertyChange("trackPrintSwitchListComment", old, enable);
914    }
915
916    /**
917     * Returns all of the rolling stock type names serviced by this track.
918     *
919     * @return rolling stock type names
920     */
921    public String[] getTypeNames() {
922        List<String> list = new ArrayList<>();
923        for (String typeName : _typeList) {
924            if (_location.acceptsTypeName(typeName)) {
925                list.add(typeName);
926            }
927        }
928        return list.toArray(new String[0]);
929    }
930
931    private void setTypeNames(String[] types) {
932        if (types.length > 0) {
933            Arrays.sort(types);
934            for (String type : types) {
935                if (!_typeList.contains(type)) {
936                    _typeList.add(type);
937                }
938            }
939        }
940    }
941
942    private void clearTypeNames() {
943        _typeList.clear();
944    }
945
946    public void addTypeName(String type) {
947        // insert at start of list, sort later
948        if (type == null || _typeList.contains(type)) {
949            return;
950        }
951        _typeList.add(0, type);
952        log.debug("Track ({}) add rolling stock type ({})", getName(), type);
953        setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() - 1, _typeList.size());
954    }
955
956    public void deleteTypeName(String type) {
957        if (_typeList.remove(type)) {
958            log.debug("Track ({}) delete rolling stock type ({})", getName(), type);
959            setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _typeList.size() + 1, _typeList.size());
960        }
961    }
962
963    public boolean isTypeNameAccepted(String type) {
964        if (!_location.acceptsTypeName(type)) {
965            return false;
966        }
967        return _typeList.contains(type);
968    }
969
970    /**
971     * Sets the train directions that can service this track
972     *
973     * @param direction EAST, WEST, NORTH, SOUTH
974     */
975    public void setTrainDirections(int direction) {
976        int old = _trainDir;
977        _trainDir = direction;
978        if (old != direction) {
979            setDirtyAndFirePropertyChange(TRAIN_DIRECTION_CHANGED_PROPERTY, Integer.toString(old),
980                    Integer.toString(direction));
981        }
982    }
983
984    public int getTrainDirections() {
985        return _trainDir;
986    }
987
988    public String getRoadOption() {
989        return _roadOption;
990    }
991
992    public String getRoadOptionString() {
993        String s;
994        if (getRoadOption().equals(Track.INCLUDE_ROADS)) {
995            s = Bundle.getMessage("AcceptOnly") + " " + getRoadNames().length + " " + Bundle.getMessage("Roads");
996        } else if (getRoadOption().equals(Track.EXCLUDE_ROADS)) {
997            s = Bundle.getMessage("Exclude") + " " + getRoadNames().length + " " + Bundle.getMessage("Roads");
998        } else {
999            s = Bundle.getMessage("AcceptsAllRoads");
1000        }
1001        return s;
1002    }
1003
1004    /**
1005     * Set the road option for this track.
1006     *
1007     * @param option ALLROADS, INCLUDEROADS, or EXCLUDEROADS
1008     */
1009    public void setRoadOption(String option) {
1010        String old = _roadOption;
1011        _roadOption = option;
1012        setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, old, option);
1013    }
1014
1015    public String[] getRoadNames() {
1016        String[] roads = _roadList.toArray(new String[0]);
1017        if (_roadList.size() > 0) {
1018            Arrays.sort(roads);
1019        }
1020        return roads;
1021    }
1022
1023    private void setRoadNames(String[] roads) {
1024        if (roads.length > 0) {
1025            Arrays.sort(roads);
1026            for (String roadName : roads) {
1027                if (!roadName.equals(NONE)) {
1028                    _roadList.add(roadName);
1029                }
1030            }
1031        }
1032    }
1033
1034    public void addRoadName(String road) {
1035        if (!_roadList.contains(road)) {
1036            _roadList.add(road);
1037            log.debug("Track ({}) add car road ({})", getName(), road);
1038            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _roadList.size() - 1, _roadList.size());
1039        }
1040    }
1041
1042    public void deleteRoadName(String road) {
1043        if (_roadList.remove(road)) {
1044            log.debug("Track ({}) delete car road ({})", getName(), road);
1045            setDirtyAndFirePropertyChange(ROADS_CHANGED_PROPERTY, _roadList.size() + 1, _roadList.size());
1046        }
1047    }
1048
1049    public boolean isRoadNameAccepted(String road) {
1050        if (getRoadOption().equals(ALL_ROADS)) {
1051            return true;
1052        }
1053        if (getRoadOption().equals(INCLUDE_ROADS)) {
1054            return _roadList.contains(road);
1055        }
1056        // exclude!
1057        return !_roadList.contains(road);
1058    }
1059
1060    public boolean containsRoadName(String road) {
1061        return _roadList.contains(road);
1062    }
1063
1064    /**
1065     * Gets the car receive load option for this track.
1066     *
1067     * @return ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1068     */
1069    public String getLoadOption() {
1070        return _loadOption;
1071    }
1072
1073    public String getLoadOptionString() {
1074        String s;
1075        if (getLoadOption().equals(Track.INCLUDE_LOADS)) {
1076            s = Bundle.getMessage("AcceptOnly") + " " + getLoadNames().length + " " + Bundle.getMessage("Loads");
1077        } else if (getLoadOption().equals(Track.EXCLUDE_LOADS)) {
1078            s = Bundle.getMessage("Exclude") + " " + getLoadNames().length + " " + Bundle.getMessage("Loads");
1079        } else {
1080            s = Bundle.getMessage("AcceptsAllLoads");
1081        }
1082        return s;
1083    }
1084
1085    /**
1086     * Set how this track deals with receiving car loads
1087     *
1088     * @param option ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1089     */
1090    public void setLoadOption(String option) {
1091        String old = _loadOption;
1092        _loadOption = option;
1093        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, old, option);
1094    }
1095
1096    private void setLoadNames(String[] loads) {
1097        if (loads.length > 0) {
1098            Arrays.sort(loads);
1099            for (String loadName : loads) {
1100                if (!loadName.equals(NONE)) {
1101                    _loadList.add(loadName);
1102                }
1103            }
1104        }
1105    }
1106
1107    /**
1108     * Provides a list of receive loads that the track will either service or
1109     * exclude. See setLoadOption
1110     *
1111     * @return Array of load names as Strings
1112     */
1113    public String[] getLoadNames() {
1114        String[] loads = _loadList.toArray(new String[0]);
1115        if (_loadList.size() > 0) {
1116            Arrays.sort(loads);
1117        }
1118        return loads;
1119    }
1120
1121    /**
1122     * Add a receive load that the track will either service or exclude. See
1123     * setLoadOption
1124     * 
1125     * @param load The string load name.
1126     */
1127    public void addLoadName(String load) {
1128        if (!_loadList.contains(load)) {
1129            _loadList.add(load);
1130            log.debug("track ({}) add car load ({})", getName(), load);
1131            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() - 1, _loadList.size());
1132        }
1133    }
1134
1135    /**
1136     * Delete a receive load name that the track will either service or exclude.
1137     * See setLoadOption
1138     * 
1139     * @param load The string load name.
1140     */
1141    public void deleteLoadName(String load) {
1142        if (_loadList.remove(load)) {
1143            log.debug("track ({}) delete car load ({})", getName(), load);
1144            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _loadList.size() + 1, _loadList.size());
1145        }
1146    }
1147
1148    /**
1149     * Determine if track will service a specific receive load name.
1150     *
1151     * @param load the load name to check.
1152     * @return true if track will service this load.
1153     */
1154    public boolean isLoadNameAccepted(String load) {
1155        if (getLoadOption().equals(ALL_LOADS)) {
1156            return true;
1157        }
1158        if (getLoadOption().equals(INCLUDE_LOADS)) {
1159            return _loadList.contains(load);
1160        }
1161        // exclude!
1162        return !_loadList.contains(load);
1163    }
1164
1165    /**
1166     * Determine if track will service a specific receive load and car type.
1167     *
1168     * @param load the load name to check.
1169     * @param type the type of car used to carry the load.
1170     * @return true if track will service this load.
1171     */
1172    public boolean isLoadNameAndCarTypeAccepted(String load, String type) {
1173        if (getLoadOption().equals(ALL_LOADS)) {
1174            return true;
1175        }
1176        if (getLoadOption().equals(INCLUDE_LOADS)) {
1177            return _loadList.contains(load) || _loadList.contains(type + CarLoad.SPLIT_CHAR + load);
1178        }
1179        // exclude!
1180        return !_loadList.contains(load) && !_loadList.contains(type + CarLoad.SPLIT_CHAR + load);
1181    }
1182
1183    /**
1184     * Gets the car ship load option for this track.
1185     *
1186     * @return ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1187     */
1188    public String getShipLoadOption() {
1189        if (!isStaging()) {
1190            return ALL_LOADS;
1191        }
1192        return _shipLoadOption;
1193    }
1194
1195    public String getShipLoadOptionString() {
1196        String s;
1197        if (getShipLoadOption().equals(Track.INCLUDE_LOADS)) {
1198            s = Bundle.getMessage("ShipOnly") + " " + getShipLoadNames().length + " " + Bundle.getMessage("Loads");
1199        } else if (getShipLoadOption().equals(Track.EXCLUDE_LOADS)) {
1200            s = Bundle.getMessage("Exclude") + " " + getShipLoadNames().length + " " + Bundle.getMessage("Loads");
1201        } else {
1202            s = Bundle.getMessage("ShipsAllLoads");
1203        }
1204        return s;
1205    }
1206
1207    /**
1208     * Set how this track deals with shipping car loads
1209     *
1210     * @param option ALL_LOADS INCLUDE_LOADS EXCLUDE_LOADS
1211     */
1212    public void setShipLoadOption(String option) {
1213        String old = _shipLoadOption;
1214        _shipLoadOption = option;
1215        setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, old, option);
1216    }
1217
1218    private void setShipLoadNames(String[] loads) {
1219        if (loads.length > 0) {
1220            Arrays.sort(loads);
1221            for (String shipLoadName : loads) {
1222                if (!shipLoadName.equals(NONE)) {
1223                    _shipLoadList.add(shipLoadName);
1224                }
1225            }
1226        }
1227    }
1228
1229    /**
1230     * Provides a list of ship loads that the track will either service or
1231     * exclude. See setShipLoadOption
1232     *
1233     * @return Array of load names as Strings
1234     */
1235    public String[] getShipLoadNames() {
1236        String[] loads = _shipLoadList.toArray(new String[0]);
1237        if (_shipLoadList.size() > 0) {
1238            Arrays.sort(loads);
1239        }
1240        return loads;
1241    }
1242
1243    /**
1244     * Add a ship load that the track will either service or exclude. See
1245     * setShipLoadOption
1246     * 
1247     * @param load The string load name.
1248     */
1249    public void addShipLoadName(String load) {
1250        if (!_shipLoadList.contains(load)) {
1251            _shipLoadList.add(load);
1252            log.debug("track ({}) add car load ({})", getName(), load);
1253            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _shipLoadList.size() - 1, _shipLoadList.size());
1254        }
1255    }
1256
1257    /**
1258     * Delete a ship load name that the track will either service or exclude.
1259     * See setLoadOption
1260     * 
1261     * @param load The string load name.
1262     */
1263    public void deleteShipLoadName(String load) {
1264        if (_shipLoadList.remove(load)) {
1265            log.debug("track ({}) delete car load ({})", getName(), load);
1266            setDirtyAndFirePropertyChange(LOADS_CHANGED_PROPERTY, _shipLoadList.size() + 1, _shipLoadList.size());
1267        }
1268    }
1269
1270    /**
1271     * Determine if track will service a specific ship load name.
1272     *
1273     * @param load the load name to check.
1274     * @return true if track will service this load.
1275     */
1276    public boolean isLoadNameShipped(String load) {
1277        if (getShipLoadOption().equals(ALL_LOADS)) {
1278            return true;
1279        }
1280        if (getShipLoadOption().equals(INCLUDE_LOADS)) {
1281            return _shipLoadList.contains(load);
1282        }
1283        // exclude!
1284        return !_shipLoadList.contains(load);
1285    }
1286
1287    /**
1288     * Determine if track will service a specific ship load and car type.
1289     *
1290     * @param load the load name to check.
1291     * @param type the type of car used to carry the load.
1292     * @return true if track will service this load.
1293     */
1294    public boolean isLoadNameAndCarTypeShipped(String load, String type) {
1295        if (getShipLoadOption().equals(ALL_LOADS)) {
1296            return true;
1297        }
1298        if (getShipLoadOption().equals(INCLUDE_LOADS)) {
1299            return _shipLoadList.contains(load) || _shipLoadList.contains(type + CarLoad.SPLIT_CHAR + load);
1300        }
1301        // exclude!
1302        return !_shipLoadList.contains(load) && !_shipLoadList.contains(type + CarLoad.SPLIT_CHAR + load);
1303    }
1304
1305    /**
1306     * Gets the drop option for this track. ANY means that all trains and routes
1307     * can drop cars to this track. The other four options are used to restrict
1308     * the track to certain trains or routes.
1309     * 
1310     * @return ANY, TRAINS, ROUTES, EXCLUDE_TRAINS, or EXCLUDE_ROUTES
1311     */
1312    public String getDropOption() {
1313        if (isYard()) {
1314            return ANY;
1315        }
1316        return _dropOption;
1317    }
1318
1319    /**
1320     * Set the car drop option for this track.
1321     *
1322     * @param option ANY, TRAINS, ROUTES, EXCLUDE_TRAINS, or EXCLUDE_ROUTES
1323     */
1324    public void setDropOption(String option) {
1325        String old = _dropOption;
1326        _dropOption = option;
1327        if (!old.equals(option)) {
1328            _dropList.clear();
1329        }
1330        setDirtyAndFirePropertyChange(DROP_CHANGED_PROPERTY, old, option);
1331    }
1332
1333    /**
1334     * Gets the pickup option for this track. ANY means that all trains and
1335     * routes can pull cars from this track. The other four options are used to
1336     * restrict the track to certain trains or routes.
1337     * 
1338     * @return ANY, TRAINS, ROUTES, EXCLUDE_TRAINS, or EXCLUDE_ROUTES
1339     */
1340    public String getPickupOption() {
1341        if (isYard()) {
1342            return ANY;
1343        }
1344        return _pickupOption;
1345    }
1346
1347    /**
1348     * Set the car pick up option for this track.
1349     *
1350     * @param option ANY, TRAINS, ROUTES, EXCLUDE_TRAINS, or EXCLUDE_ROUTES
1351     */
1352    public void setPickupOption(String option) {
1353        String old = _pickupOption;
1354        _pickupOption = option;
1355        if (!old.equals(option)) {
1356            _pickupList.clear();
1357        }
1358        setDirtyAndFirePropertyChange(PICKUP_CHANGED_PROPERTY, old, option);
1359    }
1360
1361    public String[] getDropIds() {
1362        return _dropList.toArray(new String[0]);
1363    }
1364
1365    private void setDropIds(String[] ids) {
1366        for (String id : ids) {
1367            if (id != null) {
1368                _dropList.add(id);
1369            }
1370        }
1371    }
1372
1373    public void addDropId(String id) {
1374        if (!_dropList.contains(id)) {
1375            _dropList.add(id);
1376            log.debug("Track ({}) add drop id: {}", getName(), id);
1377            setDirtyAndFirePropertyChange(DROP_CHANGED_PROPERTY, null, id);
1378        }
1379    }
1380
1381    public void deleteDropId(String id) {
1382        if (_dropList.remove(id)) {
1383            log.debug("Track ({}) delete drop id: {}", getName(), id);
1384            setDirtyAndFirePropertyChange(DROP_CHANGED_PROPERTY, id, null);
1385        }
1386    }
1387
1388    /**
1389     * Determine if train can set out cars to this track. Based on the train's
1390     * id or train's route id. See setDropOption(option).
1391     * 
1392     * @param train The Train to test.
1393     * @return true if the train can set out cars to this track.
1394     */
1395    public boolean isDropTrainAccepted(Train train) {
1396        if (getDropOption().equals(ANY)) {
1397            return true;
1398        }
1399        if (getDropOption().equals(TRAINS)) {
1400            return containsDropId(train.getId());
1401        }
1402        if (getDropOption().equals(EXCLUDE_TRAINS)) {
1403            return !containsDropId(train.getId());
1404        } else if (train.getRoute() == null) {
1405            return false;
1406        }
1407        return isDropRouteAccepted(train.getRoute());
1408    }
1409
1410    public boolean isDropRouteAccepted(Route route) {
1411        if (getDropOption().equals(ANY) || getDropOption().equals(TRAINS) || getDropOption().equals(EXCLUDE_TRAINS)) {
1412            return true;
1413        }
1414        if (getDropOption().equals(EXCLUDE_ROUTES)) {
1415            return !containsDropId(route.getId());
1416        }
1417        return containsDropId(route.getId());
1418    }
1419
1420    public boolean containsDropId(String id) {
1421        return _dropList.contains(id);
1422    }
1423
1424    public String[] getPickupIds() {
1425        return _pickupList.toArray(new String[0]);
1426    }
1427
1428    private void setPickupIds(String[] ids) {
1429        for (String id : ids) {
1430            if (id != null) {
1431                _pickupList.add(id);
1432            }
1433        }
1434    }
1435
1436    /**
1437     * Add train or route id to this track.
1438     * 
1439     * @param id The string id for the train or route.
1440     */
1441    public void addPickupId(String id) {
1442        if (!_pickupList.contains(id)) {
1443            _pickupList.add(id);
1444            log.debug("track ({}) add pick up id {}", getName(), id);
1445            setDirtyAndFirePropertyChange(PICKUP_CHANGED_PROPERTY, null, id);
1446        }
1447    }
1448
1449    public void deletePickupId(String id) {
1450        if (_pickupList.remove(id)) {
1451            log.debug("track ({}) delete pick up id {}", getName(), id);
1452            setDirtyAndFirePropertyChange(PICKUP_CHANGED_PROPERTY, id, null);
1453        }
1454    }
1455
1456    /**
1457     * Determine if train can pick up cars from this track. Based on the train's
1458     * id or train's route id. See setPickupOption(option).
1459     * 
1460     * @param train The Train to test.
1461     * @return true if the train can pick up cars from this track.
1462     */
1463    public boolean isPickupTrainAccepted(Train train) {
1464        if (getPickupOption().equals(ANY)) {
1465            return true;
1466        }
1467        if (getPickupOption().equals(TRAINS)) {
1468            return containsPickupId(train.getId());
1469        }
1470        if (getPickupOption().equals(EXCLUDE_TRAINS)) {
1471            return !containsPickupId(train.getId());
1472        } else if (train.getRoute() == null) {
1473            return false;
1474        }
1475        return isPickupRouteAccepted(train.getRoute());
1476    }
1477
1478    public boolean isPickupRouteAccepted(Route route) {
1479        if (getPickupOption().equals(ANY) ||
1480                getPickupOption().equals(TRAINS) ||
1481                getPickupOption().equals(EXCLUDE_TRAINS)) {
1482            return true;
1483        }
1484        if (getPickupOption().equals(EXCLUDE_ROUTES)) {
1485            return !containsPickupId(route.getId());
1486        }
1487        return containsPickupId(route.getId());
1488    }
1489
1490    public boolean containsPickupId(String id) {
1491        return _pickupList.contains(id);
1492    }
1493
1494    /**
1495     * Checks to see if all car types can be pulled from this track
1496     * 
1497     * @return PICKUP_OKAY if any train can pull all car types from this track
1498     */
1499    public String checkPickups() {
1500        String status = PICKUP_OKAY;
1501        S1: for (String carType : InstanceManager.getDefault(CarTypes.class).getNames()) {
1502            if (!isTypeNameAccepted(carType)) {
1503                continue;
1504            }
1505            for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByNameList()) {
1506                if (!train.isTypeNameAccepted(carType) || !isPickupTrainAccepted(train)) {
1507                    continue;
1508                }
1509                // does the train services this location and track?
1510                Route route = train.getRoute();
1511                if (route != null) {
1512                    for (RouteLocation rLoc : route.getLocationsBySequenceList()) {
1513                        if (rLoc.getName().equals(getLocation().getName()) &&
1514                                rLoc.isPickUpAllowed() &&
1515                                rLoc.getMaxCarMoves() > 0 &&
1516                                !train.isLocationSkipped(rLoc) &&
1517                                ((getTrainDirections() & rLoc.getTrainDirection()) != 0 || train.isLocalSwitcher()) &&
1518                                ((getLocation().getTrainDirections() & rLoc.getTrainDirection()) != 0 ||
1519                                        train.isLocalSwitcher())) {
1520
1521                            continue S1; // car type serviced by this train, try
1522                                         // next car type
1523                        }
1524                    }
1525                }
1526            }
1527            // None of the trains servicing this track can pick up car type
1528            status = Bundle.getMessage("ErrorNoTrain", getName(), carType);
1529            break;
1530        }
1531        return status;
1532    }
1533
1534    /**
1535     * A track has four priorities: PRIORITY_HIGH, PRIORITY_MEDIUM,
1536     * PRIORITY_NORMAL, and PRIORITY_LOW. Cars are serviced from a location
1537     * based on the track priority. Default is normal.
1538     * 
1539     * @return track priority
1540     */
1541    public String getTrackPriority() {
1542        return _trackPriority;
1543    }
1544
1545    public void setTrackPriority(String priority) {
1546        String old = _trackPriority;
1547        _trackPriority = priority;
1548        setDirtyAndFirePropertyChange(PRIORITY_CHANGED_PROPERTY, old, priority);
1549    }
1550
1551    /**
1552     * Used to determine if track can service the rolling stock.
1553     *
1554     * @param rs the car or loco to be tested
1555     * @return Error string starting with TYPE, ROAD, CAPACITY, LENGTH,
1556     *         DESTINATION or LOAD if there's an issue. OKAY if track can
1557     *         service Rolling Stock.
1558     */
1559    public String isRollingStockAccepted(RollingStock rs) {
1560        // first determine if rolling stock can be move to the new location
1561        // note that there's code that checks for certain issues by checking the
1562        // first word of the status string returned
1563        if (!isTypeNameAccepted(rs.getTypeName())) {
1564            log.debug("Rolling stock ({}) type ({}) not accepted at location ({}, {}) wrong type", rs.toString(),
1565                    rs.getTypeName(), getLocation().getName(), getName()); // NOI18N
1566            return TYPE + " (" + rs.getTypeName() + ")";
1567        }
1568        if (!isRoadNameAccepted(rs.getRoadName())) {
1569            log.debug("Rolling stock ({}) road ({}) not accepted at location ({}, {}) wrong road", rs.toString(),
1570                    rs.getRoadName(), getLocation().getName(), getName()); // NOI18N
1571            return ROAD + " (" + rs.getRoadName() + ")";
1572        }
1573        // now determine if there's enough space for the rolling stock
1574        int rsLength = rs.getTotalLength();
1575        // error check
1576        try {
1577            Integer.parseInt(rs.getLength());
1578        } catch (Exception e) {
1579            return LENGTH + " (" + rs.getLength() + ")";
1580        }
1581
1582        if (Car.class.isInstance(rs)) {
1583            Car car = (Car) rs;
1584            // does this track service the car's final destination?
1585            if (!isDestinationAccepted(car.getFinalDestination())) {
1586                // && getLocation() != car.getFinalDestination()) { // 4/14/2014
1587                // I can't remember why this was needed
1588                return DESTINATION +
1589                        " (" +
1590                        car.getFinalDestinationName() +
1591                        ") " +
1592                        Bundle.getMessage("carIsNotAllowed", getName()); // no
1593            }
1594            // does this track accept cars without a final destination?
1595            if (isOnlyCarsWithFinalDestinationEnabled() &&
1596                    car.getFinalDestination() == null &&
1597                    !car.isCaboose() &&
1598                    !car.hasFred()) {
1599                return NO_FINAL_DESTINATION;
1600            }
1601            // check for car in kernel
1602            if (car.isLead()) {
1603                rsLength = car.getKernel().getTotalLength();
1604            }
1605            if (!isLoadNameAndCarTypeAccepted(car.getLoadName(), car.getTypeName())) {
1606                log.debug("Car ({}) load ({}) not accepted at location ({}, {})", rs.toString(), car.getLoadName(),
1607                        getLocation(), getName()); // NOI18N
1608                return LOAD + " (" + car.getLoadName() + ")";
1609            }
1610        }
1611        // check for loco in consist
1612        if (Engine.class.isInstance(rs)) {
1613            Engine eng = (Engine) rs;
1614            if (eng.isLead()) {
1615                rsLength = eng.getConsist().getTotalLength();
1616            }
1617        }
1618        if (rs.getTrack() != this &&
1619                rs.getDestinationTrack() != this) {
1620            if (getUsedLength() + getReserved() + rsLength > getLength() ||
1621                    getReservedLengthSetouts() + rsLength > getLength()) {
1622                // not enough track length check to see if track is in a pool
1623                if (getPool() != null && getPool().requestTrackLength(this, rsLength)) {
1624                    return OKAY;
1625                }
1626                // ignore used length option?
1627                if (checkPlannedPickUps(rsLength)) {
1628                    return OKAY;
1629                }
1630                // Is rolling stock too long for this track?
1631                if ((getLength() < rsLength && getPool() == null) ||
1632                        (getPool() != null && getPool().getTotalLengthTracks() < rsLength)) {
1633                    return Bundle.getMessage("capacityIssue",
1634                            CAPACITY, rsLength, Setup.getLengthUnit().toLowerCase(), getLength());
1635                }
1636
1637                // The code assumes everything is fine with the track if the Length issue is returned.
1638                log.debug("Rolling stock ({}) not accepted at location ({}, {}) no room!", rs.toString(),
1639                        getLocation().getName(), getName()); // NOI18N
1640
1641                return Bundle.getMessage("lengthIssue",
1642                        LENGTH, rsLength, Setup.getLengthUnit().toLowerCase(), getAvailableTrackSpace(), getLength());
1643            }
1644        }
1645        return OKAY;
1646    }
1647
1648    /**
1649     * Performs two checks, number of new set outs shouldn't exceed the track
1650     * length. The second check protects against overloading, the total number
1651     * of cars shouldn't exceed the track length plus the number of cars to
1652     * ignore.
1653     * 
1654     * @param length rolling stock length
1655     * @return true if the program should ignore some percentage of the car's
1656     *         length currently consuming track space.
1657     */
1658    private boolean checkPlannedPickUps(int length) {
1659        if (getIgnoreUsedLengthPercentage() > IGNORE_0 && getAvailableTrackSpace() >= length) {
1660            return true;
1661        }
1662        return false;
1663    }
1664
1665    /**
1666     * Available track space. Adjusted when a track is using the planned pickups
1667     * feature
1668     * 
1669     * @return available track space
1670     */
1671    public int getAvailableTrackSpace() {
1672        // calculate the available space
1673        int available = getLength() -
1674                (getUsedLength() * (IGNORE_100 - getIgnoreUsedLengthPercentage()) / IGNORE_100 + getReserved());
1675        // could be less if track is overloaded
1676        int available3 = getLength() +
1677                (getLength() * getIgnoreUsedLengthPercentage() / IGNORE_100) -
1678                getUsedLength() -
1679                getReserved();
1680        if (available3 < available) {
1681            available = available3;
1682        }
1683        // could be less based on track length
1684        int available2 = getLength() - getReservedLengthSetouts();
1685        if (available2 < available) {
1686            available = available2;
1687        }
1688        return available;
1689    }
1690
1691    public int getMoves() {
1692        return _moves;
1693    }
1694
1695    public void setMoves(int moves) {
1696        int old = _moves;
1697        _moves = moves;
1698        setDirtyAndFirePropertyChange("trackMoves", old, moves); // NOI18N
1699    }
1700
1701    public void bumpMoves() {
1702        setMoves(getMoves() + 1);
1703    }
1704
1705    /**
1706     * Gets the blocking order for this track. Default is zero, in that case,
1707     * tracks are sorted by name.
1708     * 
1709     * @return the blocking order
1710     */
1711    public int getBlockingOrder() {
1712        return _blockingOrder;
1713    }
1714
1715    public void setBlockingOrder(int order) {
1716        int old = _blockingOrder;
1717        _blockingOrder = order;
1718        setDirtyAndFirePropertyChange(TRACK_BLOCKING_ORDER_CHANGED_PROPERTY, old, order); // NOI18N
1719    }
1720
1721    /**
1722     * Get the service order for this track. Yards and interchange have this
1723     * feature for cars. Staging has this feature for trains.
1724     *
1725     * @return Service order: Track.NORMAL, Track.FIFO, Track.LIFO
1726     */
1727    public String getServiceOrder() {
1728        if (isSpur() || (isStaging() && getPool() == null)) {
1729            return NORMAL;
1730        }
1731        return _order;
1732    }
1733
1734    /**
1735     * Set the service order for this track. Only yards and interchange have
1736     * this feature.
1737     * 
1738     * @param order Track.NORMAL, Track.FIFO, Track.LIFO
1739     */
1740    public void setServiceOrder(String order) {
1741        String old = _order;
1742        _order = order;
1743        setDirtyAndFirePropertyChange(SERVICE_ORDER_CHANGED_PROPERTY, old, order); // NOI18N
1744    }
1745
1746    /**
1747     * Returns the name of the schedule. Note that this returns the schedule
1748     * name based on the schedule's id. A schedule's name can be modified by the
1749     * user.
1750     *
1751     * @return Schedule name
1752     */
1753    public String getScheduleName() {
1754        if (getScheduleId().equals(NONE)) {
1755            return NONE;
1756        }
1757        Schedule schedule = getSchedule();
1758        if (schedule == null) {
1759            log.error("No name schedule for id: {}", getScheduleId());
1760            return NONE;
1761        }
1762        return schedule.getName();
1763    }
1764
1765    public Schedule getSchedule() {
1766        if (getScheduleId().equals(NONE)) {
1767            return null;
1768        }
1769        Schedule schedule = InstanceManager.getDefault(ScheduleManager.class).getScheduleById(getScheduleId());
1770        if (schedule == null) {
1771            log.error("No schedule for id: {}", getScheduleId());
1772        }
1773        return schedule;
1774    }
1775
1776    public void setSchedule(Schedule schedule) {
1777        String scheduleId = NONE;
1778        if (schedule != null) {
1779            scheduleId = schedule.getId();
1780        }
1781        setScheduleId(scheduleId);
1782    }
1783
1784    public String getScheduleId() {
1785        // Only spurs can have a schedule
1786        if (!isSpur()) {
1787            return NONE;
1788        }
1789        // old code only stored schedule name, so create id if needed.
1790        if (_scheduleId.equals(NONE) && !_scheduleName.equals(NONE)) {
1791            Schedule schedule = InstanceManager.getDefault(ScheduleManager.class).getScheduleByName(_scheduleName);
1792            if (schedule == null) {
1793                log.error("No schedule for name: {}", _scheduleName);
1794            } else {
1795                _scheduleId = schedule.getId();
1796            }
1797        }
1798        return _scheduleId;
1799    }
1800
1801    public void setScheduleId(String id) {
1802        String old = _scheduleId;
1803        _scheduleId = id;
1804        if (!old.equals(id)) {
1805            Schedule schedule = InstanceManager.getDefault(ScheduleManager.class).getScheduleById(id);
1806            if (schedule == null) {
1807                _scheduleName = NONE;
1808            } else {
1809                // set the sequence to the first item in the list
1810                if (schedule.getItemsBySequenceList().size() > 0) {
1811                    setScheduleItemId(schedule.getItemsBySequenceList().get(0).getId());
1812                }
1813                setScheduleCount(0);
1814            }
1815            setDirtyAndFirePropertyChange(SCHEDULE_ID_CHANGED_PROPERTY, old, id);
1816        }
1817    }
1818
1819    /**
1820     * Recommend getCurrentScheduleItem() to get the current schedule item for
1821     * this track. Protects against user deleting a schedule item from the
1822     * schedule.
1823     *
1824     * @return schedule item id
1825     */
1826    public String getScheduleItemId() {
1827        return _scheduleItemId;
1828    }
1829
1830    public void setScheduleItemId(String id) {
1831        log.debug("Set schedule item id ({}) for track ({})", id, getName());
1832        String old = _scheduleItemId;
1833        _scheduleItemId = id;
1834        setDirtyAndFirePropertyChange(SCHEDULE_CHANGED_PROPERTY, old, id);
1835    }
1836
1837    /**
1838     * Get's the current schedule item for this track Protects against user
1839     * deleting an item in a shared schedule. Recommend using this versus
1840     * getScheduleItemId() as the id can be obsolete.
1841     * 
1842     * @return The current ScheduleItem.
1843     */
1844    public ScheduleItem getCurrentScheduleItem() {
1845        Schedule sch = getSchedule();
1846        if (sch == null) {
1847            log.debug("Can not find schedule id: ({}) assigned to track ({})", getScheduleId(), getName());
1848            return null;
1849        }
1850        ScheduleItem currentSi = sch.getItemById(getScheduleItemId());
1851        if (currentSi == null && sch.getSize() > 0) {
1852            log.debug("Can not find schedule item id: ({}) for schedule ({})", getScheduleItemId(), getScheduleName());
1853            // reset schedule
1854            setScheduleItemId((sch.getItemsBySequenceList().get(0)).getId());
1855            currentSi = sch.getItemById(getScheduleItemId());
1856        }
1857        return currentSi;
1858    }
1859
1860    /**
1861     * Increments the schedule count if there's a schedule and the schedule is
1862     * running in sequential mode. Resets the schedule count if the maximum is
1863     * reached and then goes to the next item in the schedule's list.
1864     */
1865    public void bumpSchedule() {
1866        if (getSchedule() != null && getScheduleMode() == SEQUENTIAL) {
1867            // bump the schedule count
1868            setScheduleCount(getScheduleCount() + 1);
1869            if (getScheduleCount() >= getCurrentScheduleItem().getCount()) {
1870                setScheduleCount(0);
1871                // go to the next item in the schedule
1872                getNextScheduleItem();
1873            }
1874        }
1875    }
1876
1877    public ScheduleItem getNextScheduleItem() {
1878        Schedule sch = getSchedule();
1879        if (sch == null) {
1880            log.warn("Can not find schedule ({}) assigned to track ({})", getScheduleId(), getName());
1881            return null;
1882        }
1883        List<ScheduleItem> items = sch.getItemsBySequenceList();
1884        ScheduleItem nextSi = null;
1885        for (int i = 0; i < items.size(); i++) {
1886            nextSi = items.get(i);
1887            if (getCurrentScheduleItem() == nextSi) {
1888                if (++i < items.size()) {
1889                    nextSi = items.get(i);
1890                } else {
1891                    nextSi = items.get(0);
1892                }
1893                setScheduleItemId(nextSi.getId());
1894                break;
1895            }
1896        }
1897        return nextSi;
1898    }
1899
1900    /**
1901     * Returns how many times the current schedule item has been accessed.
1902     *
1903     * @return count
1904     */
1905    public int getScheduleCount() {
1906        return _scheduleCount;
1907    }
1908
1909    public void setScheduleCount(int count) {
1910        int old = _scheduleCount;
1911        _scheduleCount = count;
1912        setDirtyAndFirePropertyChange(SCHEDULE_CHANGED_PROPERTY, old, count);
1913    }
1914
1915    /**
1916     * Check to see if schedule is valid for the track at this location.
1917     *
1918     * @return SCHEDULE_OKAY if schedule okay, otherwise an error message.
1919     */
1920    public String checkScheduleValid() {
1921        if (getScheduleId().equals(NONE)) {
1922            return Schedule.SCHEDULE_OKAY;
1923        }
1924        Schedule schedule = getSchedule();
1925        if (schedule == null) {
1926            return Bundle.getMessage("CanNotFindSchedule", getScheduleId());
1927        }
1928        return schedule.checkScheduleValid(this);
1929    }
1930
1931    /**
1932     * Checks to see if car can be placed on this spur using this schedule.
1933     * Returns OKAY if the schedule can service the car.
1934     * 
1935     * @param car The Car to be tested.
1936     * @return Track.OKAY track.CUSTOM track.SCHEDULE
1937     */
1938    public String checkSchedule(Car car) {
1939        // does car already have this destination?
1940        if (car.getDestinationTrack() == this) {
1941            return OKAY;
1942        }
1943        // only spurs can have a schedule
1944        if (!isSpur()) {
1945            return OKAY;
1946        }
1947        if (getScheduleId().equals(NONE)) {
1948            // does car have a custom load?
1949            if (car.getLoadName().equals(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName()) ||
1950                    car.getLoadName().equals(InstanceManager.getDefault(CarLoads.class).getDefaultLoadName())) {
1951                return OKAY; // no
1952            }
1953            return Bundle.getMessage("carHasA", CUSTOM, LOAD, car.getLoadName());
1954        }
1955        log.debug("Track ({}) has schedule ({}) mode {} ({})", getName(), getScheduleName(), getScheduleMode(),
1956                getScheduleModeName()); // NOI18N
1957
1958        ScheduleItem si = getCurrentScheduleItem();
1959        // code check, should never be null
1960        if (si == null) {
1961            log.error("Could not find schedule item id: ({}) for schedule ({})", getScheduleItemId(),
1962                    getScheduleName()); // NOI18N
1963            return SCHEDULE + " ERROR"; // NOI18N
1964        }
1965        if (getScheduleMode() == SEQUENTIAL) {
1966            return getSchedule().checkScheduleItem(si, car, this);
1967        }
1968        // schedule in is match mode search entire schedule for a match
1969        return getSchedule().searchSchedule(car, this);
1970    }
1971
1972    /**
1973     * Check to see if track has schedule and if it does will schedule the next
1974     * item in the list. Load the car with the next schedule load if one exists,
1975     * and set the car's final destination if there's one in the schedule.
1976     * 
1977     * @param car The Car to be modified.
1978     * @return Track.OKAY or Track.SCHEDULE
1979     */
1980    public String scheduleNext(Car car) {
1981        // clean up the car's final destination if sent to that destination and
1982        // there isn't a schedule
1983        if (getScheduleId().equals(NONE) &&
1984                car.getDestination() != null &&
1985                car.getDestination().equals(car.getFinalDestination()) &&
1986                car.getDestinationTrack() != null &&
1987                (car.getDestinationTrack().equals(car.getFinalDestinationTrack()) ||
1988                        car.getFinalDestinationTrack() == null)) {
1989            car.setFinalDestination(null);
1990            car.setFinalDestinationTrack(null);
1991        }
1992        // check for schedule, only spurs can have a schedule
1993        if (getScheduleId().equals(NONE) || getSchedule() == null) {
1994            return OKAY;
1995        }
1996        // is car part of a kernel?
1997        if (car.getKernel() != null && !car.isLead()) {
1998            log.debug("Car ({}) is part of kernel ({}) not lead", car.toString(), car.getKernelName());
1999            return OKAY;
2000        }
2001        if (!car.getScheduleItemId().equals(Car.NONE)) {
2002            log.debug("Car ({}) has schedule item id ({})", car.toString(), car.getScheduleItemId());
2003            ScheduleItem si = car.getScheduleItem(this);
2004            if (si != null) {
2005                car.loadNext(si);
2006                return OKAY;
2007            }
2008            log.debug("Schedule id ({}) not valid for track ({})", car.getScheduleItemId(), getName());
2009            car.setScheduleItemId(Car.NONE);
2010        }
2011        // search schedule if match mode
2012        if (getScheduleMode() == MATCH && !getSchedule().searchSchedule(car, this).equals(OKAY)) {
2013            return Bundle.getMessage("matchMessage", SCHEDULE, getScheduleName(),
2014                            getSchedule().hasRandomItem() ? Bundle.getMessage("Random") : "");
2015        }
2016        ScheduleItem currentSi = getCurrentScheduleItem();
2017        log.debug("Destination track ({}) has schedule ({}) item id ({}) mode: {} ({})", getName(), getScheduleName(),
2018                getScheduleItemId(), getScheduleMode(), getScheduleModeName()); // NOI18N
2019        if (currentSi != null &&
2020                (currentSi.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) ||
2021                        InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
2022                                .equals(currentSi.getSetoutTrainScheduleId())) &&
2023                car.getTypeName().equals(currentSi.getTypeName()) &&
2024                (currentSi.getRoadName().equals(ScheduleItem.NONE) ||
2025                        car.getRoadName().equals(currentSi.getRoadName())) &&
2026                (currentSi.getReceiveLoadName().equals(ScheduleItem.NONE) ||
2027                        car.getLoadName().equals(currentSi.getReceiveLoadName()))) {
2028            car.setScheduleItemId(currentSi.getId());
2029            car.loadNext(currentSi);
2030            // bump schedule
2031            bumpSchedule();
2032        } else if (currentSi != null) {
2033            // build return failure message
2034            String scheduleName = "";
2035            String currentTrainScheduleName = "";
2036            TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class)
2037                    .getScheduleById(InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId());
2038            if (sch != null) {
2039                scheduleName = sch.getName();
2040            }
2041            sch = InstanceManager.getDefault(TrainScheduleManager.class)
2042                    .getScheduleById(currentSi.getSetoutTrainScheduleId());
2043            if (sch != null) {
2044                currentTrainScheduleName = sch.getName();
2045            }
2046            return Bundle.getMessage("sequentialMessage", SCHEDULE, getScheduleName(), getScheduleModeName(),
2047                    car.toString(), car.getTypeName(), scheduleName, car.getRoadName(), car.getLoadName(),
2048                    currentSi.getTypeName(), currentTrainScheduleName, currentSi.getRoadName(),
2049                    currentSi.getReceiveLoadName());
2050        } else {
2051            log.error("ERROR Track {} current schedule item is null!", getName());
2052            return SCHEDULE + " ERROR Track " + getName() + " current schedule item is null!"; // NOI18N
2053        }
2054        return OKAY;
2055    }
2056
2057    public static final String TRAIN_SCHEDULE = "trainSchedule"; // NOI18N
2058    public static final String ALL = "all"; // NOI18N
2059
2060    public boolean checkScheduleAttribute(String attribute, String carType, Car car) {
2061        Schedule schedule = getSchedule();
2062        if (schedule == null) {
2063            return true;
2064        }
2065        // if car is already placed at track, don't check car type and load
2066        if (car != null && car.getTrack() == this) {
2067            return true;
2068        }
2069        return schedule.checkScheduleAttribute(attribute, carType, car);
2070    }
2071
2072    /**
2073     * Enable changing the car generic load state when car arrives at this
2074     * track.
2075     *
2076     * @param enable when true, swap generic car load state
2077     */
2078    public void setLoadSwapEnabled(boolean enable) {
2079        boolean old = isLoadSwapEnabled();
2080        if (enable) {
2081            _loadOptions = _loadOptions | SWAP_GENERIC_LOADS;
2082        } else {
2083            _loadOptions = _loadOptions & 0xFFFF - SWAP_GENERIC_LOADS;
2084        }
2085        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2086    }
2087
2088    public boolean isLoadSwapEnabled() {
2089        return (0 != (_loadOptions & SWAP_GENERIC_LOADS));
2090    }
2091
2092    /**
2093     * Enable setting the car generic load state to empty when car arrives at
2094     * this track.
2095     *
2096     * @param enable when true, set generic car load to empty
2097     */
2098    public void setLoadEmptyEnabled(boolean enable) {
2099        boolean old = isLoadEmptyEnabled();
2100        if (enable) {
2101            _loadOptions = _loadOptions | EMPTY_GENERIC_LOADS;
2102        } else {
2103            _loadOptions = _loadOptions & 0xFFFF - EMPTY_GENERIC_LOADS;
2104        }
2105        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2106    }
2107
2108    public boolean isLoadEmptyEnabled() {
2109        return (0 != (_loadOptions & EMPTY_GENERIC_LOADS));
2110    }
2111
2112    /**
2113     * When enabled, remove Scheduled car loads.
2114     *
2115     * @param enable when true, remove Scheduled loads from cars
2116     */
2117    public void setRemoveCustomLoadsEnabled(boolean enable) {
2118        boolean old = isRemoveCustomLoadsEnabled();
2119        if (enable) {
2120            _loadOptions = _loadOptions | EMPTY_CUSTOM_LOADS;
2121        } else {
2122            _loadOptions = _loadOptions & 0xFFFF - EMPTY_CUSTOM_LOADS;
2123        }
2124        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2125    }
2126
2127    public boolean isRemoveCustomLoadsEnabled() {
2128        return (0 != (_loadOptions & EMPTY_CUSTOM_LOADS));
2129    }
2130
2131    /**
2132     * When enabled, add custom car loads if there's a demand.
2133     *
2134     * @param enable when true, add custom loads to cars
2135     */
2136    public void setAddCustomLoadsEnabled(boolean enable) {
2137        boolean old = isAddCustomLoadsEnabled();
2138        if (enable) {
2139            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS;
2140        } else {
2141            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS;
2142        }
2143        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2144    }
2145
2146    public boolean isAddCustomLoadsEnabled() {
2147        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS));
2148    }
2149
2150    /**
2151     * When enabled, add custom car loads if there's a demand by any
2152     * spur/industry.
2153     *
2154     * @param enable when true, add custom loads to cars
2155     */
2156    public void setAddCustomLoadsAnySpurEnabled(boolean enable) {
2157        boolean old = isAddCustomLoadsAnySpurEnabled();
2158        if (enable) {
2159            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS_ANY_SPUR;
2160        } else {
2161            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS_ANY_SPUR;
2162        }
2163        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2164    }
2165
2166    public boolean isAddCustomLoadsAnySpurEnabled() {
2167        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS_ANY_SPUR));
2168    }
2169
2170    /**
2171     * When enabled, add custom car loads to cars in staging for new
2172     * destinations that are staging.
2173     *
2174     * @param enable when true, add custom load to car
2175     */
2176    public void setAddCustomLoadsAnyStagingTrackEnabled(boolean enable) {
2177        boolean old = isAddCustomLoadsAnyStagingTrackEnabled();
2178        if (enable) {
2179            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK;
2180        } else {
2181            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK;
2182        }
2183        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2184    }
2185
2186    public boolean isAddCustomLoadsAnyStagingTrackEnabled() {
2187        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK));
2188    }
2189
2190    public boolean isModifyLoadsEnabled() {
2191        return isLoadEmptyEnabled() ||
2192                isLoadSwapEnabled() ||
2193                isRemoveCustomLoadsEnabled() ||
2194                isAddCustomLoadsAnySpurEnabled() ||
2195                isAddCustomLoadsAnyStagingTrackEnabled() ||
2196                isAddCustomLoadsEnabled();
2197    }
2198
2199    public void setDisableLoadChangeEnabled(boolean enable) {
2200        boolean old = isDisableLoadChangeEnabled();
2201        if (enable) {
2202            _loadOptions = _loadOptions | DISABLE_LOAD_CHANGE;
2203        } else {
2204            _loadOptions = _loadOptions & 0xFFFF - DISABLE_LOAD_CHANGE;
2205        }
2206        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2207    }
2208
2209    public boolean isDisableLoadChangeEnabled() {
2210        return (0 != (_loadOptions & DISABLE_LOAD_CHANGE));
2211    }
2212
2213    public void setQuickServiceEnabled(boolean enable) {
2214        boolean old = isQuickServiceEnabled();
2215        if (enable) {
2216            _loadOptions = _loadOptions | QUICK_SERVICE;
2217        } else {
2218            _loadOptions = _loadOptions & 0xFFFF - QUICK_SERVICE;
2219        }
2220        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2221    }
2222
2223    public boolean isQuickServiceEnabled() {
2224        return (isSpur() || isInterchange()) && !isAlternate() && (0 != (_loadOptions & QUICK_SERVICE));
2225    }
2226
2227    public void setBlockCarsEnabled(boolean enable) {
2228        boolean old = isBlockCarsEnabled();
2229        if (enable) {
2230            _blockOptions = _blockOptions | BLOCK_CARS;
2231        } else {
2232            _blockOptions = _blockOptions & 0xFFFF - BLOCK_CARS;
2233        }
2234        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2235    }
2236
2237    /**
2238     * When enabled block cars from staging.
2239     *
2240     * @return true if blocking is enabled.
2241     */
2242    public boolean isBlockCarsEnabled() {
2243        if (isStaging()) {
2244            return (0 != (_blockOptions & BLOCK_CARS));
2245        }
2246        return false;
2247    }
2248
2249    public void setPool(Pool pool) {
2250        Pool old = _pool;
2251        _pool = pool;
2252        if (old != pool) {
2253            if (old != null) {
2254                old.remove(this);
2255            }
2256            if (_pool != null) {
2257                _pool.add(this);
2258            }
2259            setDirtyAndFirePropertyChange(POOL_CHANGED_PROPERTY, old, pool);
2260        }
2261    }
2262
2263    public Pool getPool() {
2264        return _pool;
2265    }
2266
2267    public String getPoolName() {
2268        if (getPool() != null) {
2269            return getPool().getName();
2270        }
2271        return NONE;
2272    }
2273
2274    public int getDestinationListSize() {
2275        return _destinationIdList.size();
2276    }
2277
2278    /**
2279     * adds a location to the list of acceptable destinations for this track.
2280     * 
2281     * @param destination location that is acceptable
2282     */
2283    public void addDestination(Location destination) {
2284        if (!_destinationIdList.contains(destination.getId())) {
2285            _destinationIdList.add(destination.getId());
2286            setDirtyAndFirePropertyChange(DESTINATIONS_CHANGED_PROPERTY, null, destination.getName()); // NOI18N
2287        }
2288    }
2289
2290    public void deleteDestination(Location destination) {
2291        if (_destinationIdList.remove(destination.getId())) {
2292            setDirtyAndFirePropertyChange(DESTINATIONS_CHANGED_PROPERTY, destination.getName(), null); // NOI18N
2293        }
2294    }
2295
2296    /**
2297     * Returns true if destination is valid from this track.
2298     * 
2299     * @param destination The Location to be checked.
2300     * @return true if track services the destination
2301     */
2302    public boolean isDestinationAccepted(Location destination) {
2303        if (getDestinationOption().equals(ALL_DESTINATIONS) || destination == null) {
2304            return true;
2305        }
2306        return _destinationIdList.contains(destination.getId());
2307    }
2308
2309    public void setDestinationIds(String[] ids) {
2310        for (String id : ids) {
2311            _destinationIdList.add(id);
2312        }
2313    }
2314
2315    public String[] getDestinationIds() {
2316        String[] ids = _destinationIdList.toArray(new String[0]);
2317        return ids;
2318    }
2319
2320    /**
2321     * Sets the destination option for this track. The three options are:
2322     * <p>
2323     * ALL_DESTINATIONS which means this track services all destinations, the
2324     * default.
2325     * <p>
2326     * INCLUDE_DESTINATIONS which means this track services only certain
2327     * destinations.
2328     * <p>
2329     * EXCLUDE_DESTINATIONS which means this track does not service certain
2330     * destinations.
2331     *
2332     * @param option Track.ALL_DESTINATIONS, Track.INCLUDE_DESTINATIONS, or
2333     *               Track.EXCLUDE_DESTINATIONS
2334     */
2335    public void setDestinationOption(String option) {
2336        String old = _destinationOption;
2337        _destinationOption = option;
2338        if (!option.equals(old)) {
2339            setDirtyAndFirePropertyChange(DESTINATION_OPTIONS_CHANGED_PROPERTY, old, option); // NOI18N
2340        }
2341    }
2342
2343    /**
2344     * Get destination option for interchange or staging track
2345     * 
2346     * @return option
2347     */
2348    public String getDestinationOption() {
2349        if (isInterchange() || isStaging()) {
2350            return _destinationOption;
2351        }
2352        return ALL_DESTINATIONS;
2353    }
2354
2355    public void setOnlyCarsWithFinalDestinationEnabled(boolean enable) {
2356        boolean old = _onlyCarsWithFD;
2357        _onlyCarsWithFD = enable;
2358        setDirtyAndFirePropertyChange(ROUTED_CHANGED_PROPERTY, old, enable);
2359    }
2360
2361    /**
2362     * When true the track will only accept cars that have a final destination
2363     * that can be serviced by the track. See acceptsDestination(Location).
2364     * 
2365     * @return false if any car spotted, true if only cars with a FD.
2366     */
2367    public boolean isOnlyCarsWithFinalDestinationEnabled() {
2368        if (isInterchange() || isStaging()) {
2369            return _onlyCarsWithFD;
2370        }
2371        return false;
2372    }
2373
2374    /**
2375     * Used to determine if track has been assigned as an alternate
2376     *
2377     * @return true if track is an alternate
2378     */
2379    public boolean isAlternate() {
2380        for (Track track : getLocation().getTracksList()) {
2381            if (track.getAlternateTrack() == this) {
2382                return true;
2383            }
2384        }
2385        return false;
2386    }
2387
2388    public void dispose() {
2389        // change the name in case object is still in use, for example
2390        // ScheduleItem.java
2391        setName(Bundle.getMessage("NotValid", getName()));
2392        setPool(null);
2393        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, DISPOSE_CHANGED_PROPERTY);
2394    }
2395
2396    /**
2397     * Construct this Entry from XML. This member has to remain synchronized
2398     * with the detailed DTD in operations-location.dtd.
2399     *
2400     * @param e        Consist XML element
2401     * @param location The Location loading this track.
2402     */
2403    public Track(Element e, Location location) {
2404        _location = location;
2405        Attribute a;
2406        if ((a = e.getAttribute(Xml.ID)) != null) {
2407            _id = a.getValue();
2408        } else {
2409            log.warn("no id attribute in track element when reading operations");
2410        }
2411        if ((a = e.getAttribute(Xml.NAME)) != null) {
2412            _name = a.getValue();
2413        }
2414        if ((a = e.getAttribute(Xml.TRACK_TYPE)) != null) {
2415            _trackType = a.getValue();
2416
2417            // old way of storing track type before 4.21.1
2418        } else if ((a = e.getAttribute(Xml.LOC_TYPE)) != null) {
2419            if (a.getValue().equals(SIDING)) {
2420                _trackType = SPUR;
2421            } else {
2422                _trackType = a.getValue();
2423            }
2424        }
2425
2426        if ((a = e.getAttribute(Xml.LENGTH)) != null) {
2427            try {
2428                _length = Integer.parseInt(a.getValue());
2429            } catch (NumberFormatException nfe) {
2430                log.error("Track length isn't a vaild number for track {}", getName());
2431            }
2432        }
2433        if ((a = e.getAttribute(Xml.MOVES)) != null) {
2434            try {
2435                _moves = Integer.parseInt(a.getValue());
2436            } catch (NumberFormatException nfe) {
2437                log.error("Track moves isn't a vaild number for track {}", getName());
2438            }
2439
2440        }
2441        if ((a = e.getAttribute(Xml.TRACK_PRIORITY)) != null) {
2442            _trackPriority = a.getValue();
2443        }
2444        if ((a = e.getAttribute(Xml.BLOCKING_ORDER)) != null) {
2445            try {
2446                _blockingOrder = Integer.parseInt(a.getValue());
2447            } catch (NumberFormatException nfe) {
2448                log.error("Track blocking order isn't a vaild number for track {}", getName());
2449            }
2450        }
2451        if ((a = e.getAttribute(Xml.DIR)) != null) {
2452            try {
2453                _trainDir = Integer.parseInt(a.getValue());
2454            } catch (NumberFormatException nfe) {
2455                log.error("Track service direction isn't a vaild number for track {}", getName());
2456            }
2457        }
2458        // old way of reading track comment, see comments below for new format
2459        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
2460            _comment = a.getValue();
2461        }
2462        // new way of reading car types using elements added in 3.3.1
2463        if (e.getChild(Xml.TYPES) != null) {
2464            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
2465            String[] types = new String[carTypes.size()];
2466            for (int i = 0; i < carTypes.size(); i++) {
2467                Element type = carTypes.get(i);
2468                if ((a = type.getAttribute(Xml.NAME)) != null) {
2469                    types[i] = a.getValue();
2470                }
2471            }
2472            setTypeNames(types);
2473            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
2474            types = new String[locoTypes.size()];
2475            for (int i = 0; i < locoTypes.size(); i++) {
2476                Element type = locoTypes.get(i);
2477                if ((a = type.getAttribute(Xml.NAME)) != null) {
2478                    types[i] = a.getValue();
2479                }
2480            }
2481            setTypeNames(types);
2482        } // old way of reading car types up to version 3.2
2483        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
2484            String names = a.getValue();
2485            String[] types = names.split("%%"); // NOI18N
2486            setTypeNames(types);
2487        }
2488        if ((a = e.getAttribute(Xml.CAR_LOAD_OPTION)) != null) {
2489            _loadOption = a.getValue();
2490        }
2491        // new way of reading car loads using elements
2492        if (e.getChild(Xml.CAR_LOADS) != null) {
2493            List<Element> carLoads = e.getChild(Xml.CAR_LOADS).getChildren(Xml.CAR_LOAD);
2494            String[] loads = new String[carLoads.size()];
2495            for (int i = 0; i < carLoads.size(); i++) {
2496                Element load = carLoads.get(i);
2497                if ((a = load.getAttribute(Xml.NAME)) != null) {
2498                    loads[i] = a.getValue();
2499                }
2500            }
2501            setLoadNames(loads);
2502        } // old way of reading car loads up to version 3.2
2503        else if ((a = e.getAttribute(Xml.CAR_LOADS)) != null) {
2504            String names = a.getValue();
2505            String[] loads = names.split("%%"); // NOI18N
2506            log.debug("Track ({}) {} car loads: {}", getName(), getLoadOption(), names);
2507            setLoadNames(loads);
2508        }
2509        if ((a = e.getAttribute(Xml.CAR_SHIP_LOAD_OPTION)) != null) {
2510            _shipLoadOption = a.getValue();
2511        }
2512        // new way of reading car loads using elements
2513        if (e.getChild(Xml.CAR_SHIP_LOADS) != null) {
2514            List<Element> carLoads = e.getChild(Xml.CAR_SHIP_LOADS).getChildren(Xml.CAR_LOAD);
2515            String[] loads = new String[carLoads.size()];
2516            for (int i = 0; i < carLoads.size(); i++) {
2517                Element load = carLoads.get(i);
2518                if ((a = load.getAttribute(Xml.NAME)) != null) {
2519                    loads[i] = a.getValue();
2520                }
2521            }
2522            setShipLoadNames(loads);
2523        }
2524        // new way of reading drop ids using elements
2525        if (e.getChild(Xml.DROP_IDS) != null) {
2526            List<Element> dropIds = e.getChild(Xml.DROP_IDS).getChildren(Xml.DROP_ID);
2527            String[] ids = new String[dropIds.size()];
2528            for (int i = 0; i < dropIds.size(); i++) {
2529                Element dropId = dropIds.get(i);
2530                if ((a = dropId.getAttribute(Xml.ID)) != null) {
2531                    ids[i] = a.getValue();
2532                }
2533            }
2534            setDropIds(ids);
2535        } // old way of reading drop ids up to version 3.2
2536        else if ((a = e.getAttribute(Xml.DROP_IDS)) != null) {
2537            String names = a.getValue();
2538            String[] ids = names.split("%%"); // NOI18N
2539            setDropIds(ids);
2540        }
2541        if ((a = e.getAttribute(Xml.DROP_OPTION)) != null) {
2542            _dropOption = a.getValue();
2543        }
2544
2545        // new way of reading pick up ids using elements
2546        if (e.getChild(Xml.PICKUP_IDS) != null) {
2547            List<Element> pickupIds = e.getChild(Xml.PICKUP_IDS).getChildren(Xml.PICKUP_ID);
2548            String[] ids = new String[pickupIds.size()];
2549            for (int i = 0; i < pickupIds.size(); i++) {
2550                Element pickupId = pickupIds.get(i);
2551                if ((a = pickupId.getAttribute(Xml.ID)) != null) {
2552                    ids[i] = a.getValue();
2553                }
2554            }
2555            setPickupIds(ids);
2556        } // old way of reading pick up ids up to version 3.2
2557        else if ((a = e.getAttribute(Xml.PICKUP_IDS)) != null) {
2558            String names = a.getValue();
2559            String[] ids = names.split("%%"); // NOI18N
2560            setPickupIds(ids);
2561        }
2562        if ((a = e.getAttribute(Xml.PICKUP_OPTION)) != null) {
2563            _pickupOption = a.getValue();
2564        }
2565
2566        // new way of reading car roads using elements
2567        if (e.getChild(Xml.CAR_ROADS) != null) {
2568            List<Element> carRoads = e.getChild(Xml.CAR_ROADS).getChildren(Xml.CAR_ROAD);
2569            String[] roads = new String[carRoads.size()];
2570            for (int i = 0; i < carRoads.size(); i++) {
2571                Element road = carRoads.get(i);
2572                if ((a = road.getAttribute(Xml.NAME)) != null) {
2573                    roads[i] = a.getValue();
2574                }
2575            }
2576            setRoadNames(roads);
2577        } // old way of reading car roads up to version 3.2
2578        else if ((a = e.getAttribute(Xml.CAR_ROADS)) != null) {
2579            String names = a.getValue();
2580            String[] roads = names.split("%%"); // NOI18N
2581            setRoadNames(roads);
2582        }
2583        if ((a = e.getAttribute(Xml.CAR_ROAD_OPTION)) != null) {
2584            _roadOption = a.getValue();
2585        } else if ((a = e.getAttribute(Xml.CAR_ROAD_OPERATION)) != null) {
2586            _roadOption = a.getValue();
2587        }
2588
2589        if ((a = e.getAttribute(Xml.SCHEDULE)) != null) {
2590            _scheduleName = a.getValue();
2591        }
2592        if ((a = e.getAttribute(Xml.SCHEDULE_ID)) != null) {
2593            _scheduleId = a.getValue();
2594        }
2595        if ((a = e.getAttribute(Xml.ITEM_ID)) != null) {
2596            _scheduleItemId = a.getValue();
2597        }
2598        if ((a = e.getAttribute(Xml.ITEM_COUNT)) != null) {
2599            try {
2600                _scheduleCount = Integer.parseInt(a.getValue());
2601            } catch (NumberFormatException nfe) {
2602                log.error("Schedule count isn't a vaild number for track {}", getName());
2603            }
2604        }
2605        if ((a = e.getAttribute(Xml.FACTOR)) != null) {
2606            try {
2607                _reservationFactor = Integer.parseInt(a.getValue());
2608            } catch (NumberFormatException nfe) {
2609                log.error("Reservation factor isn't a vaild number for track {}", getName());
2610            }
2611        }
2612        if ((a = e.getAttribute(Xml.SCHEDULE_MODE)) != null) {
2613            try {
2614                _mode = Integer.parseInt(a.getValue());
2615            } catch (NumberFormatException nfe) {
2616                log.error("Schedule mode isn't a vaild number for track {}", getName());
2617            }
2618        }
2619        if ((a = e.getAttribute(Xml.HOLD_CARS_CUSTOM)) != null) {
2620            setHoldCarsWithCustomLoadsEnabled(a.getValue().equals(Xml.TRUE));
2621        }
2622        if ((a = e.getAttribute(Xml.ONLY_CARS_WITH_FD)) != null) {
2623            setOnlyCarsWithFinalDestinationEnabled(a.getValue().equals(Xml.TRUE));
2624        }
2625
2626        if ((a = e.getAttribute(Xml.ALTERNATIVE)) != null) {
2627            _alternateTrackId = a.getValue();
2628        }
2629
2630        if ((a = e.getAttribute(Xml.LOAD_OPTIONS)) != null) {
2631            try {
2632                _loadOptions = Integer.parseInt(a.getValue());
2633            } catch (NumberFormatException nfe) {
2634                log.error("Load options isn't a vaild number for track {}", getName());
2635            }
2636        }
2637        if ((a = e.getAttribute(Xml.BLOCK_OPTIONS)) != null) {
2638            try {
2639                _blockOptions = Integer.parseInt(a.getValue());
2640            } catch (NumberFormatException nfe) {
2641                log.error("Block options isn't a vaild number for track {}", getName());
2642            }
2643        }
2644        if ((a = e.getAttribute(Xml.ORDER)) != null) {
2645            _order = a.getValue();
2646        }
2647        if ((a = e.getAttribute(Xml.POOL)) != null) {
2648            setPool(getLocation().addPool(a.getValue()));
2649            if ((a = e.getAttribute(Xml.MIN_LENGTH)) != null) {
2650                try {
2651                    _minimumLength = Integer.parseInt(a.getValue());
2652                } catch (NumberFormatException nfe) {
2653                    log.error("Minimum pool length isn't a vaild number for track {}", getName());
2654                }
2655            }
2656            if ((a = e.getAttribute(Xml.MAX_LENGTH)) != null) {
2657                try {
2658                    _maximumLength = Integer.parseInt(a.getValue());
2659                } catch (NumberFormatException nfe) {
2660                    log.error("Maximum pool length isn't a vaild number for track {}", getName());
2661                }
2662            }
2663        }
2664        if ((a = e.getAttribute(Xml.IGNORE_USED_PERCENTAGE)) != null) {
2665            try {
2666                _ignoreUsedLengthPercentage = Integer.parseInt(a.getValue());
2667            } catch (NumberFormatException nfe) {
2668                log.error("Ignore used percentage isn't a vaild number for track {}", getName());
2669            }
2670        }
2671        if ((a = e.getAttribute(Xml.TRACK_DESTINATION_OPTION)) != null) {
2672            _destinationOption = a.getValue();
2673        }
2674        if (e.getChild(Xml.DESTINATIONS) != null) {
2675            List<Element> eDestinations = e.getChild(Xml.DESTINATIONS).getChildren(Xml.DESTINATION);
2676            for (Element eDestination : eDestinations) {
2677                if ((a = eDestination.getAttribute(Xml.ID)) != null) {
2678                    _destinationIdList.add(a.getValue());
2679                }
2680            }
2681        }
2682
2683        if (e.getChild(Xml.COMMENTS) != null) {
2684            if (e.getChild(Xml.COMMENTS).getChild(Xml.TRACK) != null &&
2685                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.TRACK).getAttribute(Xml.COMMENT)) != null) {
2686                _comment = a.getValue();
2687            }
2688            if (e.getChild(Xml.COMMENTS).getChild(Xml.BOTH) != null &&
2689                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.BOTH).getAttribute(Xml.COMMENT)) != null) {
2690                _commentBoth = a.getValue();
2691            }
2692            if (e.getChild(Xml.COMMENTS).getChild(Xml.PICKUP) != null &&
2693                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PICKUP).getAttribute(Xml.COMMENT)) != null) {
2694                _commentPickup = a.getValue();
2695            }
2696            if (e.getChild(Xml.COMMENTS).getChild(Xml.SETOUT) != null &&
2697                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.SETOUT).getAttribute(Xml.COMMENT)) != null) {
2698                _commentSetout = a.getValue();
2699            }
2700            if (e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_MANIFEST) != null &&
2701                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_MANIFEST).getAttribute(Xml.COMMENT)) != null) {
2702                _printCommentManifest = a.getValue().equals(Xml.TRUE);
2703            }
2704            if (e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_SWITCH_LISTS) != null &&
2705                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_SWITCH_LISTS).getAttribute(Xml.COMMENT)) != null) {
2706                _printCommentSwitchList = a.getValue().equals(Xml.TRUE);
2707            }
2708        }
2709
2710        if ((a = e.getAttribute(Xml.READER)) != null) {
2711            try {
2712                Reporter r = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).provideReporter(a.getValue());
2713                _reader = r;
2714            } catch (IllegalArgumentException ex) {
2715                log.warn("Not able to find reader: {} for location ({})", a.getValue(), getName());
2716            }
2717        }
2718    }
2719
2720    /**
2721     * Create an XML element to represent this Entry. This member has to remain
2722     * synchronized with the detailed DTD in operations-location.dtd.
2723     *
2724     * @return Contents in a JDOM Element
2725     */
2726    public Element store() {
2727        Element e = new Element(Xml.TRACK);
2728        e.setAttribute(Xml.ID, getId());
2729        e.setAttribute(Xml.NAME, getName());
2730        e.setAttribute(Xml.TRACK_TYPE, getTrackType());
2731        e.setAttribute(Xml.DIR, Integer.toString(getTrainDirections()));
2732        e.setAttribute(Xml.LENGTH, Integer.toString(getLength()));
2733        e.setAttribute(Xml.MOVES, Integer.toString(getMoves() - getDropRS()));
2734        if (!getTrackPriority().equals(PRIORITY_NORMAL)) {
2735            e.setAttribute(Xml.TRACK_PRIORITY, getTrackPriority());
2736        }
2737        if (getBlockingOrder() != 0) {
2738            e.setAttribute(Xml.BLOCKING_ORDER, Integer.toString(getBlockingOrder()));
2739        }
2740        // build list of car types for this track
2741        String[] types = getTypeNames();
2742        // new way of saving car types using elements
2743        Element eTypes = new Element(Xml.TYPES);
2744        for (String type : types) {
2745            // don't save types that have been deleted by user
2746            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
2747                Element eType = new Element(Xml.LOCO_TYPE);
2748                eType.setAttribute(Xml.NAME, type);
2749                eTypes.addContent(eType);
2750            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
2751                Element eType = new Element(Xml.CAR_TYPE);
2752                eType.setAttribute(Xml.NAME, type);
2753                eTypes.addContent(eType);
2754            }
2755        }
2756        e.addContent(eTypes);
2757
2758        // build list of car roads for this track
2759        if (!getRoadOption().equals(ALL_ROADS)) {
2760            e.setAttribute(Xml.CAR_ROAD_OPTION, getRoadOption());
2761            String[] roads = getRoadNames();
2762            // new way of saving road names
2763            Element eRoads = new Element(Xml.CAR_ROADS);
2764            for (String road : roads) {
2765                Element eRoad = new Element(Xml.CAR_ROAD);
2766                eRoad.setAttribute(Xml.NAME, road);
2767                eRoads.addContent(eRoad);
2768            }
2769            e.addContent(eRoads);
2770        }
2771
2772        // save list of car loads for this track
2773        if (!getLoadOption().equals(ALL_LOADS)) {
2774            e.setAttribute(Xml.CAR_LOAD_OPTION, getLoadOption());
2775            String[] loads = getLoadNames();
2776            // new way of saving car loads using elements
2777            Element eLoads = new Element(Xml.CAR_LOADS);
2778            for (String load : loads) {
2779                Element eLoad = new Element(Xml.CAR_LOAD);
2780                eLoad.setAttribute(Xml.NAME, load);
2781                eLoads.addContent(eLoad);
2782            }
2783            e.addContent(eLoads);
2784        }
2785
2786        // save list of car loads for this track
2787        if (!getShipLoadOption().equals(ALL_LOADS)) {
2788            e.setAttribute(Xml.CAR_SHIP_LOAD_OPTION, getShipLoadOption());
2789            String[] loads = getShipLoadNames();
2790            // new way of saving car loads using elements
2791            Element eLoads = new Element(Xml.CAR_SHIP_LOADS);
2792            for (String load : loads) {
2793                Element eLoad = new Element(Xml.CAR_LOAD);
2794                eLoad.setAttribute(Xml.NAME, load);
2795                eLoads.addContent(eLoad);
2796            }
2797            e.addContent(eLoads);
2798        }
2799
2800        if (!getDropOption().equals(ANY)) {
2801            e.setAttribute(Xml.DROP_OPTION, getDropOption());
2802            // build list of drop ids for this track
2803            String[] dropIds = getDropIds();
2804            // new way of saving drop ids using elements
2805            Element eDropIds = new Element(Xml.DROP_IDS);
2806            for (String id : dropIds) {
2807                Element eDropId = new Element(Xml.DROP_ID);
2808                eDropId.setAttribute(Xml.ID, id);
2809                eDropIds.addContent(eDropId);
2810            }
2811            e.addContent(eDropIds);
2812        }
2813
2814        if (!getPickupOption().equals(ANY)) {
2815            e.setAttribute(Xml.PICKUP_OPTION, getPickupOption());
2816            // build list of pickup ids for this track
2817            String[] pickupIds = getPickupIds();
2818            // new way of saving pick up ids using elements
2819            Element ePickupIds = new Element(Xml.PICKUP_IDS);
2820            for (String id : pickupIds) {
2821                Element ePickupId = new Element(Xml.PICKUP_ID);
2822                ePickupId.setAttribute(Xml.ID, id);
2823                ePickupIds.addContent(ePickupId);
2824            }
2825            e.addContent(ePickupIds);
2826        }
2827
2828        if (getSchedule() != null) {
2829            e.setAttribute(Xml.SCHEDULE, getScheduleName());
2830            e.setAttribute(Xml.SCHEDULE_ID, getScheduleId());
2831            e.setAttribute(Xml.ITEM_ID, getScheduleItemId());
2832            e.setAttribute(Xml.ITEM_COUNT, Integer.toString(getScheduleCount()));
2833            e.setAttribute(Xml.FACTOR, Integer.toString(getReservationFactor()));
2834            e.setAttribute(Xml.SCHEDULE_MODE, Integer.toString(getScheduleMode()));
2835            e.setAttribute(Xml.HOLD_CARS_CUSTOM, isHoldCarsWithCustomLoadsEnabled() ? Xml.TRUE : Xml.FALSE);
2836        }
2837        if (isInterchange() || isStaging()) {
2838            e.setAttribute(Xml.ONLY_CARS_WITH_FD, isOnlyCarsWithFinalDestinationEnabled() ? Xml.TRUE : Xml.FALSE);
2839        }
2840        if (getAlternateTrack() != null) {
2841            e.setAttribute(Xml.ALTERNATIVE, getAlternateTrack().getId());
2842        }
2843        if (_loadOptions != 0) {
2844            e.setAttribute(Xml.LOAD_OPTIONS, Integer.toString(_loadOptions));
2845        }
2846        if (isBlockCarsEnabled()) {
2847            e.setAttribute(Xml.BLOCK_OPTIONS, Integer.toString(_blockOptions));
2848        }
2849        if (!getServiceOrder().equals(NORMAL)) {
2850            e.setAttribute(Xml.ORDER, getServiceOrder());
2851        }
2852        if (getPool() != null) {
2853            e.setAttribute(Xml.POOL, getPool().getName());
2854            e.setAttribute(Xml.MIN_LENGTH, Integer.toString(getPoolMinimumLength()));
2855            if (getPoolMaximumLength() != Integer.MAX_VALUE) {
2856                e.setAttribute(Xml.MAX_LENGTH, Integer.toString(getPoolMaximumLength()));
2857            }
2858        }
2859        if (getIgnoreUsedLengthPercentage() > IGNORE_0) {
2860            e.setAttribute(Xml.IGNORE_USED_PERCENTAGE, Integer.toString(getIgnoreUsedLengthPercentage()));
2861        }
2862
2863        if ((isStaging() || isInterchange()) && !getDestinationOption().equals(ALL_DESTINATIONS)) {
2864            e.setAttribute(Xml.TRACK_DESTINATION_OPTION, getDestinationOption());
2865            // save destinations if they exist
2866            String[] destIds = getDestinationIds();
2867            if (destIds.length > 0) {
2868                Element destinations = new Element(Xml.DESTINATIONS);
2869                for (String id : destIds) {
2870                    Location loc = InstanceManager.getDefault(LocationManager.class).getLocationById(id);
2871                    if (loc != null) {
2872                        Element destination = new Element(Xml.DESTINATION);
2873                        destination.setAttribute(Xml.ID, id);
2874                        destination.setAttribute(Xml.NAME, loc.getName());
2875                        destinations.addContent(destination);
2876                    }
2877                }
2878                e.addContent(destinations);
2879            }
2880        }
2881        // save manifest track comments if they exist
2882        if (!getComment().equals(NONE) ||
2883                !getCommentBothWithColor().equals(NONE) ||
2884                !getCommentPickupWithColor().equals(NONE) ||
2885                !getCommentSetoutWithColor().equals(NONE)) {
2886            Element comments = new Element(Xml.COMMENTS);
2887            Element track = new Element(Xml.TRACK);
2888            Element both = new Element(Xml.BOTH);
2889            Element pickup = new Element(Xml.PICKUP);
2890            Element setout = new Element(Xml.SETOUT);
2891            Element printManifest = new Element(Xml.PRINT_MANIFEST);
2892            Element printSwitchList = new Element(Xml.PRINT_SWITCH_LISTS);
2893
2894            comments.addContent(track);
2895            comments.addContent(both);
2896            comments.addContent(pickup);
2897            comments.addContent(setout);
2898            comments.addContent(printManifest);
2899            comments.addContent(printSwitchList);
2900
2901            track.setAttribute(Xml.COMMENT, getComment());
2902            both.setAttribute(Xml.COMMENT, getCommentBothWithColor());
2903            pickup.setAttribute(Xml.COMMENT, getCommentPickupWithColor());
2904            setout.setAttribute(Xml.COMMENT, getCommentSetoutWithColor());
2905            printManifest.setAttribute(Xml.COMMENT, isPrintManifestCommentEnabled() ? Xml.TRUE : Xml.FALSE);
2906            printSwitchList.setAttribute(Xml.COMMENT, isPrintSwitchListCommentEnabled() ? Xml.TRUE : Xml.FALSE);
2907
2908            e.addContent(comments);
2909        }
2910        if (getReporter() != null) {
2911            e.setAttribute(Xml.READER, getReporter().getDisplayName());
2912        }
2913        return e;
2914    }
2915
2916    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
2917        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
2918        firePropertyChange(p, old, n);
2919    }
2920
2921    /*
2922     * set the jmri.Reporter object associated with this location.
2923     *
2924     * @param reader jmri.Reporter object.
2925     */
2926    public void setReporter(Reporter r) {
2927        Reporter old = _reader;
2928        _reader = r;
2929        if (old != r) {
2930            setDirtyAndFirePropertyChange(TRACK_REPORTER_CHANGED_PROPERTY, old, r);
2931        }
2932    }
2933
2934    /*
2935     * get the jmri.Reporter object associated with this location.
2936     *
2937     * @return jmri.Reporter object.
2938     */
2939    public Reporter getReporter() {
2940        return _reader;
2941    }
2942
2943    public String getReporterName() {
2944        if (getReporter() != null) {
2945            return getReporter().getDisplayName();
2946        }
2947        return "";
2948    }
2949
2950    private final static Logger log = LoggerFactory.getLogger(Track.class);
2951
2952}