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