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