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, true);
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. Loads the car with the schedule id.
1975     * 
1976     * @param car The Car to be modified.
1977     * @return Track.OKAY or Track.SCHEDULE
1978     */
1979    public String scheduleNext(Car car) {
1980        // check for schedule, only spurs can have a schedule
1981        if (getSchedule() == null) {
1982            return OKAY;
1983        }
1984        // is car part of a kernel?
1985        if (car.getKernel() != null && !car.isLead()) {
1986            log.debug("Car ({}) is part of kernel ({}) not lead", car.toString(), car.getKernelName());
1987            return OKAY;
1988        }
1989        // has the car already been assigned to this destination?
1990        if (!car.getScheduleItemId().equals(Car.NONE)) {
1991            log.debug("Car ({}) has schedule item id ({})", car.toString(), car.getScheduleItemId());
1992            ScheduleItem si = car.getScheduleItem(this);
1993            if (si != null) {
1994                // bump hit count for this schedule item
1995                si.setHits(si.getHits() + 1);
1996                return OKAY;
1997            }
1998            log.debug("Schedule id ({}) not valid for track ({})", car.getScheduleItemId(), getName());
1999            car.setScheduleItemId(Car.NONE);
2000        }
2001        // search schedule if match mode
2002        if (getScheduleMode() == MATCH && !getSchedule().searchSchedule(car, this).equals(OKAY)) {
2003            return Bundle.getMessage("matchMessage", SCHEDULE, getScheduleName(),
2004                    getSchedule().hasRandomItem() ? Bundle.getMessage("Random") : "");
2005        }
2006        // found a match or in sequential mode
2007        ScheduleItem currentSi = getCurrentScheduleItem();
2008        log.debug("Destination track ({}) has schedule ({}) item id ({}) mode: {} ({})", getName(), getScheduleName(),
2009                getScheduleItemId(), getScheduleMode(), getScheduleModeName()); // NOI18N
2010        if (currentSi != null &&
2011                getSchedule().checkScheduleItem(currentSi, car, this, false).equals(OKAY)) {
2012            car.setScheduleItemId(currentSi.getId());
2013            // bump hit count for this schedule item
2014            currentSi.setHits(currentSi.getHits() + 1);
2015            // bump schedule
2016            bumpSchedule();
2017        } else if (currentSi != null) {
2018            // build return failure message
2019            String scheduleName = "";
2020            String currentTrainScheduleName = "";
2021            TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class)
2022                    .getScheduleById(InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId());
2023            if (sch != null) {
2024                scheduleName = sch.getName();
2025            }
2026            sch = InstanceManager.getDefault(TrainScheduleManager.class)
2027                    .getScheduleById(currentSi.getSetoutTrainScheduleId());
2028            if (sch != null) {
2029                currentTrainScheduleName = sch.getName();
2030            }
2031            return Bundle.getMessage("sequentialMessage", SCHEDULE, getScheduleName(), getScheduleModeName(),
2032                    car.toString(), car.getTypeName(), scheduleName, car.getRoadName(), car.getLoadName(),
2033                    currentSi.getTypeName(), currentTrainScheduleName, currentSi.getRoadName(),
2034                    currentSi.getReceiveLoadName());
2035        } else {
2036            log.error("ERROR Track {} current schedule item is null!", getName());
2037            return SCHEDULE + " ERROR Track " + getName() + " current schedule item is null!"; // NOI18N
2038        }
2039        return OKAY;
2040    }
2041
2042    public static final String TRAIN_SCHEDULE = "trainSchedule"; // NOI18N
2043    public static final String ALL = "all"; // NOI18N
2044
2045    public boolean checkScheduleAttribute(String attribute, String carType, Car car) {
2046        Schedule schedule = getSchedule();
2047        if (schedule == null) {
2048            return true;
2049        }
2050        // if car is already placed at track, don't check car type and load
2051        if (car != null && car.getTrack() == this) {
2052            return true;
2053        }
2054        return schedule.checkScheduleAttribute(attribute, carType, car);
2055    }
2056
2057    /**
2058     * Enable changing the car generic load state when car arrives at this
2059     * track.
2060     *
2061     * @param enable when true, swap generic car load state
2062     */
2063    public void setLoadSwapEnabled(boolean enable) {
2064        boolean old = isLoadSwapEnabled();
2065        if (enable) {
2066            _loadOptions = _loadOptions | SWAP_GENERIC_LOADS;
2067        } else {
2068            _loadOptions = _loadOptions & 0xFFFF - SWAP_GENERIC_LOADS;
2069        }
2070        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2071    }
2072
2073    public boolean isLoadSwapEnabled() {
2074        return (0 != (_loadOptions & SWAP_GENERIC_LOADS));
2075    }
2076
2077    /**
2078     * Enable setting the car generic load state to empty when car arrives at
2079     * this track.
2080     *
2081     * @param enable when true, set generic car load to empty
2082     */
2083    public void setLoadEmptyEnabled(boolean enable) {
2084        boolean old = isLoadEmptyEnabled();
2085        if (enable) {
2086            _loadOptions = _loadOptions | EMPTY_GENERIC_LOADS;
2087        } else {
2088            _loadOptions = _loadOptions & 0xFFFF - EMPTY_GENERIC_LOADS;
2089        }
2090        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2091    }
2092
2093    public boolean isLoadEmptyEnabled() {
2094        return (0 != (_loadOptions & EMPTY_GENERIC_LOADS));
2095    }
2096
2097    /**
2098     * When enabled, remove Scheduled car loads.
2099     *
2100     * @param enable when true, remove Scheduled loads from cars
2101     */
2102    public void setRemoveCustomLoadsEnabled(boolean enable) {
2103        boolean old = isRemoveCustomLoadsEnabled();
2104        if (enable) {
2105            _loadOptions = _loadOptions | EMPTY_CUSTOM_LOADS;
2106        } else {
2107            _loadOptions = _loadOptions & 0xFFFF - EMPTY_CUSTOM_LOADS;
2108        }
2109        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2110    }
2111
2112    public boolean isRemoveCustomLoadsEnabled() {
2113        return (0 != (_loadOptions & EMPTY_CUSTOM_LOADS));
2114    }
2115
2116    /**
2117     * When enabled, add custom car loads if there's a demand.
2118     *
2119     * @param enable when true, add custom loads to cars
2120     */
2121    public void setAddCustomLoadsEnabled(boolean enable) {
2122        boolean old = isAddCustomLoadsEnabled();
2123        if (enable) {
2124            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS;
2125        } else {
2126            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS;
2127        }
2128        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2129    }
2130
2131    public boolean isAddCustomLoadsEnabled() {
2132        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS));
2133    }
2134
2135    /**
2136     * When enabled, add custom car loads if there's a demand by any
2137     * spur/industry.
2138     *
2139     * @param enable when true, add custom loads to cars
2140     */
2141    public void setAddCustomLoadsAnySpurEnabled(boolean enable) {
2142        boolean old = isAddCustomLoadsAnySpurEnabled();
2143        if (enable) {
2144            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS_ANY_SPUR;
2145        } else {
2146            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS_ANY_SPUR;
2147        }
2148        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2149    }
2150
2151    public boolean isAddCustomLoadsAnySpurEnabled() {
2152        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS_ANY_SPUR));
2153    }
2154
2155    /**
2156     * When enabled, add custom car loads to cars in staging for new
2157     * destinations that are staging.
2158     *
2159     * @param enable when true, add custom load to car
2160     */
2161    public void setAddCustomLoadsAnyStagingTrackEnabled(boolean enable) {
2162        boolean old = isAddCustomLoadsAnyStagingTrackEnabled();
2163        if (enable) {
2164            _loadOptions = _loadOptions | GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK;
2165        } else {
2166            _loadOptions = _loadOptions & 0xFFFF - GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK;
2167        }
2168        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2169    }
2170
2171    public boolean isAddCustomLoadsAnyStagingTrackEnabled() {
2172        return (0 != (_loadOptions & GENERATE_CUSTOM_LOADS_ANY_STAGING_TRACK));
2173    }
2174
2175    public boolean isModifyLoadsEnabled() {
2176        return isLoadEmptyEnabled() ||
2177                isLoadSwapEnabled() ||
2178                isRemoveCustomLoadsEnabled() ||
2179                isAddCustomLoadsAnySpurEnabled() ||
2180                isAddCustomLoadsAnyStagingTrackEnabled() ||
2181                isAddCustomLoadsEnabled();
2182    }
2183
2184    public void setDisableLoadChangeEnabled(boolean enable) {
2185        boolean old = isDisableLoadChangeEnabled();
2186        if (enable) {
2187            _loadOptions = _loadOptions | DISABLE_LOAD_CHANGE;
2188        } else {
2189            _loadOptions = _loadOptions & 0xFFFF - DISABLE_LOAD_CHANGE;
2190        }
2191        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2192    }
2193
2194    public boolean isDisableLoadChangeEnabled() {
2195        return (0 != (_loadOptions & DISABLE_LOAD_CHANGE));
2196    }
2197
2198    public void setQuickServiceEnabled(boolean enable) {
2199        boolean old = isQuickServiceEnabled();
2200        if (enable) {
2201            _loadOptions = _loadOptions | QUICK_SERVICE;
2202        } else {
2203            _loadOptions = _loadOptions & 0xFFFF - QUICK_SERVICE;
2204        }
2205        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2206    }
2207
2208    public boolean isQuickServiceEnabled() {
2209        return (isSpur() || isInterchange()) && !isAlternate() && (0 != (_loadOptions & QUICK_SERVICE));
2210    }
2211
2212    public void setBlockCarsEnabled(boolean enable) {
2213        boolean old = isBlockCarsEnabled();
2214        if (enable) {
2215            _blockOptions = _blockOptions | BLOCK_CARS;
2216        } else {
2217            _blockOptions = _blockOptions & 0xFFFF - BLOCK_CARS;
2218        }
2219        setDirtyAndFirePropertyChange(LOAD_OPTIONS_CHANGED_PROPERTY, old, enable);
2220    }
2221
2222    /**
2223     * When enabled block cars from staging.
2224     *
2225     * @return true if blocking is enabled.
2226     */
2227    public boolean isBlockCarsEnabled() {
2228        if (isStaging()) {
2229            return (0 != (_blockOptions & BLOCK_CARS));
2230        }
2231        return false;
2232    }
2233
2234    public void setPool(Pool pool) {
2235        Pool old = _pool;
2236        _pool = pool;
2237        if (old != pool) {
2238            if (old != null) {
2239                old.remove(this);
2240            }
2241            if (_pool != null) {
2242                _pool.add(this);
2243            }
2244            setDirtyAndFirePropertyChange(POOL_CHANGED_PROPERTY, old, pool);
2245        }
2246    }
2247
2248    public Pool getPool() {
2249        return _pool;
2250    }
2251
2252    public String getPoolName() {
2253        if (getPool() != null) {
2254            return getPool().getName();
2255        }
2256        return NONE;
2257    }
2258
2259    public int getDestinationListSize() {
2260        return _destinationIdList.size();
2261    }
2262
2263    /**
2264     * adds a location to the list of acceptable destinations for this track.
2265     * 
2266     * @param destination location that is acceptable
2267     */
2268    public void addDestination(Location destination) {
2269        if (!_destinationIdList.contains(destination.getId())) {
2270            _destinationIdList.add(destination.getId());
2271            setDirtyAndFirePropertyChange(DESTINATIONS_CHANGED_PROPERTY, null, destination.getName()); // NOI18N
2272        }
2273    }
2274
2275    public void deleteDestination(Location destination) {
2276        if (_destinationIdList.remove(destination.getId())) {
2277            setDirtyAndFirePropertyChange(DESTINATIONS_CHANGED_PROPERTY, destination.getName(), null); // NOI18N
2278        }
2279    }
2280
2281    /**
2282     * Returns true if destination is valid from this track.
2283     * 
2284     * @param destination The Location to be checked.
2285     * @return true if track services the destination
2286     */
2287    public boolean isDestinationAccepted(Location destination) {
2288        if (getDestinationOption().equals(ALL_DESTINATIONS) || destination == null) {
2289            return true;
2290        }
2291        return _destinationIdList.contains(destination.getId());
2292    }
2293
2294    public void setDestinationIds(String[] ids) {
2295        for (String id : ids) {
2296            _destinationIdList.add(id);
2297        }
2298    }
2299
2300    public String[] getDestinationIds() {
2301        String[] ids = _destinationIdList.toArray(new String[0]);
2302        return ids;
2303    }
2304
2305    /**
2306     * Sets the destination option for this track. The three options are:
2307     * <p>
2308     * ALL_DESTINATIONS which means this track services all destinations, the
2309     * default.
2310     * <p>
2311     * INCLUDE_DESTINATIONS which means this track services only certain
2312     * destinations.
2313     * <p>
2314     * EXCLUDE_DESTINATIONS which means this track does not service certain
2315     * destinations.
2316     *
2317     * @param option Track.ALL_DESTINATIONS, Track.INCLUDE_DESTINATIONS, or
2318     *               Track.EXCLUDE_DESTINATIONS
2319     */
2320    public void setDestinationOption(String option) {
2321        String old = _destinationOption;
2322        _destinationOption = option;
2323        if (!option.equals(old)) {
2324            setDirtyAndFirePropertyChange(DESTINATION_OPTIONS_CHANGED_PROPERTY, old, option); // NOI18N
2325        }
2326    }
2327
2328    /**
2329     * Get destination option for interchange or staging track
2330     * 
2331     * @return option
2332     */
2333    public String getDestinationOption() {
2334        if (isInterchange() || isStaging()) {
2335            return _destinationOption;
2336        }
2337        return ALL_DESTINATIONS;
2338    }
2339
2340    public void setOnlyCarsWithFinalDestinationEnabled(boolean enable) {
2341        boolean old = _onlyCarsWithFD;
2342        _onlyCarsWithFD = enable;
2343        setDirtyAndFirePropertyChange(ROUTED_CHANGED_PROPERTY, old, enable);
2344    }
2345
2346    /**
2347     * When true the track will only accept cars that have a final destination
2348     * that can be serviced by the track. See acceptsDestination(Location).
2349     * 
2350     * @return false if any car spotted, true if only cars with a FD.
2351     */
2352    public boolean isOnlyCarsWithFinalDestinationEnabled() {
2353        if (isInterchange() || isStaging()) {
2354            return _onlyCarsWithFD;
2355        }
2356        return false;
2357    }
2358
2359    /**
2360     * Used to determine if track has been assigned as an alternate
2361     *
2362     * @return true if track is an alternate
2363     */
2364    public boolean isAlternate() {
2365        for (Track track : getLocation().getTracksList()) {
2366            if (track.getAlternateTrack() == this) {
2367                return true;
2368            }
2369        }
2370        return false;
2371    }
2372
2373    public void dispose() {
2374        // change the name in case object is still in use, for example
2375        // ScheduleItem.java
2376        setName(Bundle.getMessage("NotValid", getName()));
2377        setPool(null);
2378        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, DISPOSE_CHANGED_PROPERTY);
2379    }
2380
2381    /**
2382     * Construct this Entry from XML. This member has to remain synchronized
2383     * with the detailed DTD in operations-location.dtd.
2384     *
2385     * @param e        Consist XML element
2386     * @param location The Location loading this track.
2387     */
2388    public Track(Element e, Location location) {
2389        _location = location;
2390        Attribute a;
2391        if ((a = e.getAttribute(Xml.ID)) != null) {
2392            _id = a.getValue();
2393        } else {
2394            log.warn("no id attribute in track element when reading operations");
2395        }
2396        if ((a = e.getAttribute(Xml.NAME)) != null) {
2397            _name = a.getValue();
2398        }
2399        if ((a = e.getAttribute(Xml.TRACK_TYPE)) != null) {
2400            _trackType = a.getValue();
2401
2402            // old way of storing track type before 4.21.1
2403        } else if ((a = e.getAttribute(Xml.LOC_TYPE)) != null) {
2404            if (a.getValue().equals(SIDING)) {
2405                _trackType = SPUR;
2406            } else {
2407                _trackType = a.getValue();
2408            }
2409        }
2410
2411        if ((a = e.getAttribute(Xml.LENGTH)) != null) {
2412            try {
2413                _length = Integer.parseInt(a.getValue());
2414            } catch (NumberFormatException nfe) {
2415                log.error("Track length isn't a vaild number for track {}", getName());
2416            }
2417        }
2418        if ((a = e.getAttribute(Xml.MOVES)) != null) {
2419            try {
2420                _moves = Integer.parseInt(a.getValue());
2421            } catch (NumberFormatException nfe) {
2422                log.error("Track moves isn't a vaild number for track {}", getName());
2423            }
2424
2425        }
2426        if ((a = e.getAttribute(Xml.TRACK_PRIORITY)) != null) {
2427            _trackPriority = a.getValue();
2428        }
2429        if ((a = e.getAttribute(Xml.BLOCKING_ORDER)) != null) {
2430            try {
2431                _blockingOrder = Integer.parseInt(a.getValue());
2432            } catch (NumberFormatException nfe) {
2433                log.error("Track blocking order isn't a vaild number for track {}", getName());
2434            }
2435        }
2436        if ((a = e.getAttribute(Xml.DIR)) != null) {
2437            try {
2438                _trainDir = Integer.parseInt(a.getValue());
2439            } catch (NumberFormatException nfe) {
2440                log.error("Track service direction isn't a vaild number for track {}", getName());
2441            }
2442        }
2443        // old way of reading track comment, see comments below for new format
2444        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
2445            _comment = a.getValue();
2446        }
2447        // new way of reading car types using elements added in 3.3.1
2448        if (e.getChild(Xml.TYPES) != null) {
2449            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
2450            String[] types = new String[carTypes.size()];
2451            for (int i = 0; i < carTypes.size(); i++) {
2452                Element type = carTypes.get(i);
2453                if ((a = type.getAttribute(Xml.NAME)) != null) {
2454                    types[i] = a.getValue();
2455                }
2456            }
2457            setTypeNames(types);
2458            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
2459            types = new String[locoTypes.size()];
2460            for (int i = 0; i < locoTypes.size(); i++) {
2461                Element type = locoTypes.get(i);
2462                if ((a = type.getAttribute(Xml.NAME)) != null) {
2463                    types[i] = a.getValue();
2464                }
2465            }
2466            setTypeNames(types);
2467        } // old way of reading car types up to version 3.2
2468        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
2469            String names = a.getValue();
2470            String[] types = names.split("%%"); // NOI18N
2471            setTypeNames(types);
2472        }
2473        if ((a = e.getAttribute(Xml.CAR_LOAD_OPTION)) != null) {
2474            _loadOption = a.getValue();
2475        }
2476        // new way of reading car loads using elements
2477        if (e.getChild(Xml.CAR_LOADS) != null) {
2478            List<Element> carLoads = e.getChild(Xml.CAR_LOADS).getChildren(Xml.CAR_LOAD);
2479            String[] loads = new String[carLoads.size()];
2480            for (int i = 0; i < carLoads.size(); i++) {
2481                Element load = carLoads.get(i);
2482                if ((a = load.getAttribute(Xml.NAME)) != null) {
2483                    loads[i] = a.getValue();
2484                }
2485            }
2486            setLoadNames(loads);
2487        } // old way of reading car loads up to version 3.2
2488        else if ((a = e.getAttribute(Xml.CAR_LOADS)) != null) {
2489            String names = a.getValue();
2490            String[] loads = names.split("%%"); // NOI18N
2491            log.debug("Track ({}) {} car loads: {}", getName(), getLoadOption(), names);
2492            setLoadNames(loads);
2493        }
2494        if ((a = e.getAttribute(Xml.CAR_SHIP_LOAD_OPTION)) != null) {
2495            _shipLoadOption = a.getValue();
2496        }
2497        // new way of reading car loads using elements
2498        if (e.getChild(Xml.CAR_SHIP_LOADS) != null) {
2499            List<Element> carLoads = e.getChild(Xml.CAR_SHIP_LOADS).getChildren(Xml.CAR_LOAD);
2500            String[] loads = new String[carLoads.size()];
2501            for (int i = 0; i < carLoads.size(); i++) {
2502                Element load = carLoads.get(i);
2503                if ((a = load.getAttribute(Xml.NAME)) != null) {
2504                    loads[i] = a.getValue();
2505                }
2506            }
2507            setShipLoadNames(loads);
2508        }
2509        // new way of reading drop ids using elements
2510        if (e.getChild(Xml.DROP_IDS) != null) {
2511            List<Element> dropIds = e.getChild(Xml.DROP_IDS).getChildren(Xml.DROP_ID);
2512            String[] ids = new String[dropIds.size()];
2513            for (int i = 0; i < dropIds.size(); i++) {
2514                Element dropId = dropIds.get(i);
2515                if ((a = dropId.getAttribute(Xml.ID)) != null) {
2516                    ids[i] = a.getValue();
2517                }
2518            }
2519            setDropIds(ids);
2520        } // old way of reading drop ids up to version 3.2
2521        else if ((a = e.getAttribute(Xml.DROP_IDS)) != null) {
2522            String names = a.getValue();
2523            String[] ids = names.split("%%"); // NOI18N
2524            setDropIds(ids);
2525        }
2526        if ((a = e.getAttribute(Xml.DROP_OPTION)) != null) {
2527            _dropOption = a.getValue();
2528        }
2529
2530        // new way of reading pick up ids using elements
2531        if (e.getChild(Xml.PICKUP_IDS) != null) {
2532            List<Element> pickupIds = e.getChild(Xml.PICKUP_IDS).getChildren(Xml.PICKUP_ID);
2533            String[] ids = new String[pickupIds.size()];
2534            for (int i = 0; i < pickupIds.size(); i++) {
2535                Element pickupId = pickupIds.get(i);
2536                if ((a = pickupId.getAttribute(Xml.ID)) != null) {
2537                    ids[i] = a.getValue();
2538                }
2539            }
2540            setPickupIds(ids);
2541        } // old way of reading pick up ids up to version 3.2
2542        else if ((a = e.getAttribute(Xml.PICKUP_IDS)) != null) {
2543            String names = a.getValue();
2544            String[] ids = names.split("%%"); // NOI18N
2545            setPickupIds(ids);
2546        }
2547        if ((a = e.getAttribute(Xml.PICKUP_OPTION)) != null) {
2548            _pickupOption = a.getValue();
2549        }
2550
2551        // new way of reading car roads using elements
2552        if (e.getChild(Xml.CAR_ROADS) != null) {
2553            List<Element> carRoads = e.getChild(Xml.CAR_ROADS).getChildren(Xml.CAR_ROAD);
2554            String[] roads = new String[carRoads.size()];
2555            for (int i = 0; i < carRoads.size(); i++) {
2556                Element road = carRoads.get(i);
2557                if ((a = road.getAttribute(Xml.NAME)) != null) {
2558                    roads[i] = a.getValue();
2559                }
2560            }
2561            setRoadNames(roads);
2562        } // old way of reading car roads up to version 3.2
2563        else if ((a = e.getAttribute(Xml.CAR_ROADS)) != null) {
2564            String names = a.getValue();
2565            String[] roads = names.split("%%"); // NOI18N
2566            setRoadNames(roads);
2567        }
2568        if ((a = e.getAttribute(Xml.CAR_ROAD_OPTION)) != null) {
2569            _roadOption = a.getValue();
2570        } else if ((a = e.getAttribute(Xml.CAR_ROAD_OPERATION)) != null) {
2571            _roadOption = a.getValue();
2572        }
2573
2574        if ((a = e.getAttribute(Xml.SCHEDULE)) != null) {
2575            _scheduleName = a.getValue();
2576        }
2577        if ((a = e.getAttribute(Xml.SCHEDULE_ID)) != null) {
2578            _scheduleId = a.getValue();
2579        }
2580        if ((a = e.getAttribute(Xml.ITEM_ID)) != null) {
2581            _scheduleItemId = a.getValue();
2582        }
2583        if ((a = e.getAttribute(Xml.ITEM_COUNT)) != null) {
2584            try {
2585                _scheduleCount = Integer.parseInt(a.getValue());
2586            } catch (NumberFormatException nfe) {
2587                log.error("Schedule count isn't a vaild number for track {}", getName());
2588            }
2589        }
2590        if ((a = e.getAttribute(Xml.FACTOR)) != null) {
2591            try {
2592                _reservationFactor = Integer.parseInt(a.getValue());
2593            } catch (NumberFormatException nfe) {
2594                log.error("Reservation factor isn't a vaild number for track {}", getName());
2595            }
2596        }
2597        if ((a = e.getAttribute(Xml.SCHEDULE_MODE)) != null) {
2598            try {
2599                _mode = Integer.parseInt(a.getValue());
2600            } catch (NumberFormatException nfe) {
2601                log.error("Schedule mode isn't a vaild number for track {}", getName());
2602            }
2603        }
2604        if ((a = e.getAttribute(Xml.HOLD_CARS_CUSTOM)) != null) {
2605            setHoldCarsWithCustomLoadsEnabled(a.getValue().equals(Xml.TRUE));
2606        }
2607        if ((a = e.getAttribute(Xml.ONLY_CARS_WITH_FD)) != null) {
2608            setOnlyCarsWithFinalDestinationEnabled(a.getValue().equals(Xml.TRUE));
2609        }
2610
2611        if ((a = e.getAttribute(Xml.ALTERNATIVE)) != null) {
2612            _alternateTrackId = a.getValue();
2613        }
2614
2615        if ((a = e.getAttribute(Xml.LOAD_OPTIONS)) != null) {
2616            try {
2617                _loadOptions = Integer.parseInt(a.getValue());
2618            } catch (NumberFormatException nfe) {
2619                log.error("Load options isn't a vaild number for track {}", getName());
2620            }
2621        }
2622        if ((a = e.getAttribute(Xml.BLOCK_OPTIONS)) != null) {
2623            try {
2624                _blockOptions = Integer.parseInt(a.getValue());
2625            } catch (NumberFormatException nfe) {
2626                log.error("Block options isn't a vaild number for track {}", getName());
2627            }
2628        }
2629        if ((a = e.getAttribute(Xml.ORDER)) != null) {
2630            _order = a.getValue();
2631        }
2632        if ((a = e.getAttribute(Xml.POOL)) != null) {
2633            setPool(getLocation().addPool(a.getValue()));
2634            if ((a = e.getAttribute(Xml.MIN_LENGTH)) != null) {
2635                try {
2636                    _minimumLength = Integer.parseInt(a.getValue());
2637                } catch (NumberFormatException nfe) {
2638                    log.error("Minimum pool length isn't a vaild number for track {}", getName());
2639                }
2640            }
2641            if ((a = e.getAttribute(Xml.MAX_LENGTH)) != null) {
2642                try {
2643                    _maximumLength = Integer.parseInt(a.getValue());
2644                } catch (NumberFormatException nfe) {
2645                    log.error("Maximum pool length isn't a vaild number for track {}", getName());
2646                }
2647            }
2648        }
2649        if ((a = e.getAttribute(Xml.IGNORE_USED_PERCENTAGE)) != null) {
2650            try {
2651                _ignoreUsedLengthPercentage = Integer.parseInt(a.getValue());
2652            } catch (NumberFormatException nfe) {
2653                log.error("Ignore used percentage isn't a vaild number for track {}", getName());
2654            }
2655        }
2656        if ((a = e.getAttribute(Xml.TRACK_DESTINATION_OPTION)) != null) {
2657            _destinationOption = a.getValue();
2658        }
2659        if (e.getChild(Xml.DESTINATIONS) != null) {
2660            List<Element> eDestinations = e.getChild(Xml.DESTINATIONS).getChildren(Xml.DESTINATION);
2661            for (Element eDestination : eDestinations) {
2662                if ((a = eDestination.getAttribute(Xml.ID)) != null) {
2663                    _destinationIdList.add(a.getValue());
2664                }
2665            }
2666        }
2667
2668        if (e.getChild(Xml.COMMENTS) != null) {
2669            if (e.getChild(Xml.COMMENTS).getChild(Xml.TRACK) != null &&
2670                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.TRACK).getAttribute(Xml.COMMENT)) != null) {
2671                _comment = a.getValue();
2672            }
2673            if (e.getChild(Xml.COMMENTS).getChild(Xml.BOTH) != null &&
2674                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.BOTH).getAttribute(Xml.COMMENT)) != null) {
2675                _commentBoth = a.getValue();
2676            }
2677            if (e.getChild(Xml.COMMENTS).getChild(Xml.PICKUP) != null &&
2678                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PICKUP).getAttribute(Xml.COMMENT)) != null) {
2679                _commentPickup = a.getValue();
2680            }
2681            if (e.getChild(Xml.COMMENTS).getChild(Xml.SETOUT) != null &&
2682                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.SETOUT).getAttribute(Xml.COMMENT)) != null) {
2683                _commentSetout = a.getValue();
2684            }
2685            if (e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_MANIFEST) != null &&
2686                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_MANIFEST).getAttribute(Xml.COMMENT)) != null) {
2687                _printCommentManifest = a.getValue().equals(Xml.TRUE);
2688            }
2689            if (e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_SWITCH_LISTS) != null &&
2690                    (a = e.getChild(Xml.COMMENTS).getChild(Xml.PRINT_SWITCH_LISTS).getAttribute(Xml.COMMENT)) != null) {
2691                _printCommentSwitchList = a.getValue().equals(Xml.TRUE);
2692            }
2693        }
2694
2695        if ((a = e.getAttribute(Xml.READER)) != null) {
2696            try {
2697                Reporter r = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).provideReporter(a.getValue());
2698                _reader = r;
2699            } catch (IllegalArgumentException ex) {
2700                log.warn("Not able to find reader: {} for location ({})", a.getValue(), getName());
2701            }
2702        }
2703    }
2704
2705    /**
2706     * Create an XML element to represent this Entry. This member has to remain
2707     * synchronized with the detailed DTD in operations-location.dtd.
2708     *
2709     * @return Contents in a JDOM Element
2710     */
2711    public Element store() {
2712        Element e = new Element(Xml.TRACK);
2713        e.setAttribute(Xml.ID, getId());
2714        e.setAttribute(Xml.NAME, getName());
2715        e.setAttribute(Xml.TRACK_TYPE, getTrackType());
2716        e.setAttribute(Xml.DIR, Integer.toString(getTrainDirections()));
2717        e.setAttribute(Xml.LENGTH, Integer.toString(getLength()));
2718        e.setAttribute(Xml.MOVES, Integer.toString(getMoves() - getDropRS()));
2719        if (!getTrackPriority().equals(PRIORITY_NORMAL)) {
2720            e.setAttribute(Xml.TRACK_PRIORITY, getTrackPriority());
2721        }
2722        if (getBlockingOrder() != 0) {
2723            e.setAttribute(Xml.BLOCKING_ORDER, Integer.toString(getBlockingOrder()));
2724        }
2725        // build list of car types for this track
2726        String[] types = getTypeNames();
2727        // new way of saving car types using elements
2728        Element eTypes = new Element(Xml.TYPES);
2729        for (String type : types) {
2730            // don't save types that have been deleted by user
2731            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
2732                Element eType = new Element(Xml.LOCO_TYPE);
2733                eType.setAttribute(Xml.NAME, type);
2734                eTypes.addContent(eType);
2735            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
2736                Element eType = new Element(Xml.CAR_TYPE);
2737                eType.setAttribute(Xml.NAME, type);
2738                eTypes.addContent(eType);
2739            }
2740        }
2741        e.addContent(eTypes);
2742
2743        // build list of car roads for this track
2744        if (!getRoadOption().equals(ALL_ROADS)) {
2745            e.setAttribute(Xml.CAR_ROAD_OPTION, getRoadOption());
2746            String[] roads = getRoadNames();
2747            // new way of saving road names
2748            Element eRoads = new Element(Xml.CAR_ROADS);
2749            for (String road : roads) {
2750                Element eRoad = new Element(Xml.CAR_ROAD);
2751                eRoad.setAttribute(Xml.NAME, road);
2752                eRoads.addContent(eRoad);
2753            }
2754            e.addContent(eRoads);
2755        }
2756
2757        // save list of car loads for this track
2758        if (!getLoadOption().equals(ALL_LOADS)) {
2759            e.setAttribute(Xml.CAR_LOAD_OPTION, getLoadOption());
2760            String[] loads = getLoadNames();
2761            // new way of saving car loads using elements
2762            Element eLoads = new Element(Xml.CAR_LOADS);
2763            for (String load : loads) {
2764                Element eLoad = new Element(Xml.CAR_LOAD);
2765                eLoad.setAttribute(Xml.NAME, load);
2766                eLoads.addContent(eLoad);
2767            }
2768            e.addContent(eLoads);
2769        }
2770
2771        // save list of car loads for this track
2772        if (!getShipLoadOption().equals(ALL_LOADS)) {
2773            e.setAttribute(Xml.CAR_SHIP_LOAD_OPTION, getShipLoadOption());
2774            String[] loads = getShipLoadNames();
2775            // new way of saving car loads using elements
2776            Element eLoads = new Element(Xml.CAR_SHIP_LOADS);
2777            for (String load : loads) {
2778                Element eLoad = new Element(Xml.CAR_LOAD);
2779                eLoad.setAttribute(Xml.NAME, load);
2780                eLoads.addContent(eLoad);
2781            }
2782            e.addContent(eLoads);
2783        }
2784
2785        if (!getDropOption().equals(ANY)) {
2786            e.setAttribute(Xml.DROP_OPTION, getDropOption());
2787            // build list of drop ids for this track
2788            String[] dropIds = getDropIds();
2789            // new way of saving drop ids using elements
2790            Element eDropIds = new Element(Xml.DROP_IDS);
2791            for (String id : dropIds) {
2792                Element eDropId = new Element(Xml.DROP_ID);
2793                eDropId.setAttribute(Xml.ID, id);
2794                eDropIds.addContent(eDropId);
2795            }
2796            e.addContent(eDropIds);
2797        }
2798
2799        if (!getPickupOption().equals(ANY)) {
2800            e.setAttribute(Xml.PICKUP_OPTION, getPickupOption());
2801            // build list of pickup ids for this track
2802            String[] pickupIds = getPickupIds();
2803            // new way of saving pick up ids using elements
2804            Element ePickupIds = new Element(Xml.PICKUP_IDS);
2805            for (String id : pickupIds) {
2806                Element ePickupId = new Element(Xml.PICKUP_ID);
2807                ePickupId.setAttribute(Xml.ID, id);
2808                ePickupIds.addContent(ePickupId);
2809            }
2810            e.addContent(ePickupIds);
2811        }
2812
2813        if (getSchedule() != null) {
2814            e.setAttribute(Xml.SCHEDULE, getScheduleName());
2815            e.setAttribute(Xml.SCHEDULE_ID, getScheduleId());
2816            e.setAttribute(Xml.ITEM_ID, getScheduleItemId());
2817            e.setAttribute(Xml.ITEM_COUNT, Integer.toString(getScheduleCount()));
2818            e.setAttribute(Xml.FACTOR, Integer.toString(getReservationFactor()));
2819            e.setAttribute(Xml.SCHEDULE_MODE, Integer.toString(getScheduleMode()));
2820            e.setAttribute(Xml.HOLD_CARS_CUSTOM, isHoldCarsWithCustomLoadsEnabled() ? Xml.TRUE : Xml.FALSE);
2821        }
2822        if (isInterchange() || isStaging()) {
2823            e.setAttribute(Xml.ONLY_CARS_WITH_FD, isOnlyCarsWithFinalDestinationEnabled() ? Xml.TRUE : Xml.FALSE);
2824        }
2825        if (getAlternateTrack() != null) {
2826            e.setAttribute(Xml.ALTERNATIVE, getAlternateTrack().getId());
2827        }
2828        if (_loadOptions != 0) {
2829            e.setAttribute(Xml.LOAD_OPTIONS, Integer.toString(_loadOptions));
2830        }
2831        if (isBlockCarsEnabled()) {
2832            e.setAttribute(Xml.BLOCK_OPTIONS, Integer.toString(_blockOptions));
2833        }
2834        if (!getServiceOrder().equals(NORMAL)) {
2835            e.setAttribute(Xml.ORDER, getServiceOrder());
2836        }
2837        if (getPool() != null) {
2838            e.setAttribute(Xml.POOL, getPool().getName());
2839            e.setAttribute(Xml.MIN_LENGTH, Integer.toString(getPoolMinimumLength()));
2840            if (getPoolMaximumLength() != Integer.MAX_VALUE) {
2841                e.setAttribute(Xml.MAX_LENGTH, Integer.toString(getPoolMaximumLength()));
2842            }
2843        }
2844        if (getIgnoreUsedLengthPercentage() > IGNORE_0) {
2845            e.setAttribute(Xml.IGNORE_USED_PERCENTAGE, Integer.toString(getIgnoreUsedLengthPercentage()));
2846        }
2847
2848        if ((isStaging() || isInterchange()) && !getDestinationOption().equals(ALL_DESTINATIONS)) {
2849            e.setAttribute(Xml.TRACK_DESTINATION_OPTION, getDestinationOption());
2850            // save destinations if they exist
2851            String[] destIds = getDestinationIds();
2852            if (destIds.length > 0) {
2853                Element destinations = new Element(Xml.DESTINATIONS);
2854                for (String id : destIds) {
2855                    Location loc = InstanceManager.getDefault(LocationManager.class).getLocationById(id);
2856                    if (loc != null) {
2857                        Element destination = new Element(Xml.DESTINATION);
2858                        destination.setAttribute(Xml.ID, id);
2859                        destination.setAttribute(Xml.NAME, loc.getName());
2860                        destinations.addContent(destination);
2861                    }
2862                }
2863                e.addContent(destinations);
2864            }
2865        }
2866        // save manifest track comments if they exist
2867        if (!getComment().equals(NONE) ||
2868                !getCommentBothWithColor().equals(NONE) ||
2869                !getCommentPickupWithColor().equals(NONE) ||
2870                !getCommentSetoutWithColor().equals(NONE)) {
2871            Element comments = new Element(Xml.COMMENTS);
2872            Element track = new Element(Xml.TRACK);
2873            Element both = new Element(Xml.BOTH);
2874            Element pickup = new Element(Xml.PICKUP);
2875            Element setout = new Element(Xml.SETOUT);
2876            Element printManifest = new Element(Xml.PRINT_MANIFEST);
2877            Element printSwitchList = new Element(Xml.PRINT_SWITCH_LISTS);
2878
2879            comments.addContent(track);
2880            comments.addContent(both);
2881            comments.addContent(pickup);
2882            comments.addContent(setout);
2883            comments.addContent(printManifest);
2884            comments.addContent(printSwitchList);
2885
2886            track.setAttribute(Xml.COMMENT, getComment());
2887            both.setAttribute(Xml.COMMENT, getCommentBothWithColor());
2888            pickup.setAttribute(Xml.COMMENT, getCommentPickupWithColor());
2889            setout.setAttribute(Xml.COMMENT, getCommentSetoutWithColor());
2890            printManifest.setAttribute(Xml.COMMENT, isPrintManifestCommentEnabled() ? Xml.TRUE : Xml.FALSE);
2891            printSwitchList.setAttribute(Xml.COMMENT, isPrintSwitchListCommentEnabled() ? Xml.TRUE : Xml.FALSE);
2892
2893            e.addContent(comments);
2894        }
2895        if (getReporter() != null) {
2896            e.setAttribute(Xml.READER, getReporter().getDisplayName());
2897        }
2898        return e;
2899    }
2900
2901    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
2902        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
2903        firePropertyChange(p, old, n);
2904    }
2905
2906    /*
2907     * set the jmri.Reporter object associated with this location.
2908     *
2909     * @param reader jmri.Reporter object.
2910     */
2911    public void setReporter(Reporter r) {
2912        Reporter old = _reader;
2913        _reader = r;
2914        if (old != r) {
2915            setDirtyAndFirePropertyChange(TRACK_REPORTER_CHANGED_PROPERTY, old, r);
2916        }
2917    }
2918
2919    /*
2920     * get the jmri.Reporter object associated with this location.
2921     *
2922     * @return jmri.Reporter object.
2923     */
2924    public Reporter getReporter() {
2925        return _reader;
2926    }
2927
2928    public String getReporterName() {
2929        if (getReporter() != null) {
2930            return getReporter().getDisplayName();
2931        }
2932        return "";
2933    }
2934
2935    private final static Logger log = LoggerFactory.getLogger(Track.class);
2936
2937}