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