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