001package jmri.jmrit.operations.rollingstock;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.text.SimpleDateFormat;
006import java.time.LocalDateTime;
007import java.time.ZoneId;
008import java.time.format.DateTimeFormatter;
009import java.util.Date;
010
011import jmri.*;
012import jmri.beans.Identifiable;
013import jmri.beans.PropertyChangeSupport;
014import jmri.jmrit.operations.locations.*;
015import jmri.jmrit.operations.locations.divisions.Division;
016import jmri.jmrit.operations.locations.divisions.DivisionManager;
017import jmri.jmrit.operations.rollingstock.cars.*;
018import jmri.jmrit.operations.routes.RouteLocation;
019import jmri.jmrit.operations.setup.Setup;
020import jmri.jmrit.operations.trains.Train;
021import jmri.jmrit.operations.trains.TrainManager;
022import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * Represents rolling stock, both powered (locomotives) and not powered (cars)
029 * on the layout.
030 *
031 * @author Daniel Boudreau Copyright (C) 2009, 2010, 2013, 2023, 2026
032 */
033public abstract class RollingStock extends PropertyChangeSupport implements Identifiable, PropertyChangeListener {
034
035    public static final String NONE = "";
036    public static final int DEFAULT_BLOCKING_ORDER = 0;
037    public static final int MAX_BLOCKING_ORDER = 100;
038    public static final boolean FORCE = true; // ignore length, type, etc. when setting car's track
039    protected static final String DEFAULT_WEIGHT = "0";
040
041    public static final String CLONE = TrainCommon.HYPHEN + "(Clone)"; // NOI18N
042    // parentheses are special chars
043    public static final String CLONE_REGEX = TrainCommon.HYPHEN + "\\(Clone\\)"; // NOI18N
044
045    protected String _id = NONE;
046    protected String _order = NONE;
047    protected String _number = NONE;
048    protected String _road = NONE;
049    protected String _type = NONE;
050    protected String _length = "0";
051    protected String _color = "Black";
052    protected String _weight = DEFAULT_WEIGHT;
053    protected String _weightTons = DEFAULT_WEIGHT;
054    protected String _built = NONE;
055    protected String _owner = NONE;
056    protected String _comment = NONE;
057    protected String _routeId = NONE; // saved route for interchange tracks
058    protected String _rfid = NONE;
059    protected String _value = NONE;
060    protected LocalDateTime _lastDate = null;
061    protected boolean _locationUnknown = false;
062    protected boolean _outOfService = false;
063    protected boolean _selected = false;
064
065    protected Location _location = null;
066    protected Track _track = null;
067    protected Location _destination = null;
068    protected Track _trackDestination = null;
069    protected Train _train = null;
070    protected RouteLocation _routeLocation = null;
071    protected RouteLocation _routeDestination = null;
072    protected RouteLocation _routeDestinationTiming = null;
073    protected Division _division = null;
074    protected boolean _clone = false;
075    protected int _cloneOrder = 9999999;
076    protected int _moves = 0;
077    protected String _lastLocationId = LOCATION_UNKNOWN; // the rollingstock's last location id
078    protected String _lastTrackId = LOCATION_UNKNOWN; // the rollingstock's last track id
079    protected Train _lastTrain = null; // the last train moving this rs
080    protected int _blocking = DEFAULT_BLOCKING_ORDER;
081    protected String _pickupTime = NONE;
082    protected String _setoutTime = NONE;
083
084    protected IdTag _tag = null;
085    protected PropertyChangeListener _tagListener = null;
086    protected Location _whereLastSeen = null; // location reported by tag reader
087    protected LocalDateTime _whenLastSeen = null; // date reported by tag reader
088
089    public static final String LOCATION_UNKNOWN = "0";
090
091    protected int number = 0; // used by rolling stock manager for sort by number
092    private final LocalDateTime epoch = LocalDateTime.ofInstant(new java.util.GregorianCalendar().getGregorianChange().toInstant(), ZoneId.systemDefault());
093
094    public static final String ERROR_TRACK = "ERROR wrong track for location"; // checks for coding error // NOI18N
095
096    // property changes
097    public static final String TRACK_CHANGED_PROPERTY = "rolling stock track location"; // NOI18N
098    public static final String DESTINATION_TRACK_CHANGED_PROPERTY = "rolling stock track destination"; // NOI18N
099    public static final String TRAIN_CHANGED_PROPERTY = "rolling stock train"; // NOI18N
100    public static final String LENGTH_CHANGED_PROPERTY = "rolling stock length"; // NOI18N
101    public static final String TYPE_CHANGED_PROPERTY = "rolling stock type"; // NOI18N
102    public static final String ROUTE_LOCATION_CHANGED_PROPERTY = "rolling stock route location"; // NOI18N
103    public static final String ROUTE_DESTINATION_CHANGED_PROPERTY = "rolling stock route destination"; // NOI18N
104    public static final String COMMENT_CHANGED_PROPERTY = "rolling stock comment"; // NOI18N
105
106    // the draw bar length must only be calculated once at startup
107    public static final int COUPLERS = Setup.getLengthUnit().equals(Setup.FEET)
108            ? Integer.parseInt(Bundle.getMessage("DrawBarLengthFeet"))
109            : Integer.parseInt(Bundle.getMessage("DrawBarLengthMeter")); // stocks TODO catch empty/non-integer value
110
111    LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
112
113    public RollingStock() {
114        _lastDate = epoch;
115    }
116
117    public RollingStock(String road, String number) {
118        this();
119        log.debug("New rolling stock ({} {})", road, number);
120        _road = road;
121        _number = number;
122        _id = createId(road, number);
123        addPropertyChangeListeners();
124    }
125
126    public static String createId(String road, String number) {
127        return road + number;
128    }
129
130    @Override
131    public String getId() {
132        return _id;
133    }
134
135    public abstract RollingStock copy();
136
137    public RollingStock copy(RollingStock rs) {
138        rs.setBuilt(getBuilt());
139        rs.setColorName(getColorName());
140        rs.setLength(getLength());
141        rs.setWeightTons(getWeightTons());
142        rs.setNumber(getNumber());
143        rs.setOwnerName(getOwnerName());
144        rs.setRoadName(getRoadName());
145        rs.setTypeName(getTypeName());
146        rs.setComment(getComment());
147        rs.setBlocking(getBlocking());
148        rs.setLastTrain(getLastTrain());
149        rs.setLastDate(getLastDate());
150        rs.setLastLocationId(getLastLocationId());
151        rs.setLastTrackId(getLastTrackId());
152        rs.setLastRouteId(getLastRouteId());
153        rs.setDivision(getDivision());
154        return rs;
155    }
156
157    /**
158     * Set the rolling stock identification or road number
159     *
160     * @param number The rolling stock road number.
161     */
162    public void setNumber(String number) {
163        String oldNumber = _number;
164        _number = number;
165        if (!oldNumber.equals(number)) {
166            firePropertyChange("rolling stock number", oldNumber, number); // NOI18N
167            String oldId = _id;
168            _id = createId(_road, number);
169            setDirtyAndFirePropertyChange(Xml.ID, oldId, _id);
170        }
171    }
172
173    public String getNumber() {
174        return _number;
175    }
176
177    public void setRoadName(String road) {
178        String old = _road;
179        _road = road;
180        if (!old.equals(road)) {
181            firePropertyChange("rolling stock road", old, road); // NOI18N
182            String oldId = _id;
183            _id = createId(road, _number);
184            setDirtyAndFirePropertyChange(Xml.ID, oldId, _id);
185        }
186    }
187
188    public String getRoadName() {
189        return _road;
190    }
191
192    /**
193     * For combobox and identification
194     */
195    @Override
196    public String toString() {
197        return getRoadName() + " " + getNumber();
198    }
199
200    /**
201     * Sets the type of rolling stock. "Boxcar" for example is a type of car.
202     *
203     * @param type The type of rolling stock.
204     */
205    public void setTypeName(String type) {
206        String old = _type;
207        _type = type;
208        if (!old.equals(type)) {
209            setDirtyAndFirePropertyChange("rolling stock type", old, type); // NOI18N
210        }
211    }
212
213    public String getTypeName() {
214        return _type;
215    }
216
217    protected boolean _lengthChange = false; // used for loco length change
218
219    /**
220     * Sets the body length of the rolling stock. For example, a 40' boxcar
221     * would be entered as 40 feet. Coupler length is added by the program when
222     * determining if a car could fit on a track.
223     * 
224     * @see #getTotalLength()
225     * @param length the body length in feet or meters
226     */
227    public void setLength(String length) {
228        String old = _length;
229        if (!old.equals(length)) {
230            // adjust used length if rolling stock is at a location
231            if (getLocation() != null && getTrack() != null) {
232                getLocation().setUsedLength(
233                        getLocation().getUsedLength() + Integer.parseInt(length) - Integer.parseInt(old));
234                getTrack().setUsedLength(getTrack().getUsedLength() + Integer.parseInt(length) - Integer.parseInt(old));
235                if (getDestination() != null && getDestinationTrack() != null && !_lengthChange) {
236                    _lengthChange = true; // prevent recursive loop, and we want the "old" engine length
237                    log.debug("Rolling stock ({}) has destination ({}, {})", this, getDestination().getName(),
238                            getDestinationTrack().getName());
239                    getTrack().deletePickupRS(this);
240                    getDestinationTrack().deleteDropRS(this);
241                    // now change the length and update tracks
242                    _length = length;
243                    getTrack().addPickupRS(this);
244                    getDestinationTrack().addDropRS(this);
245                    _lengthChange = false; // done
246                }
247            }
248            _length = length;
249            setDirtyAndFirePropertyChange(LENGTH_CHANGED_PROPERTY, old, length);
250        }
251    }
252
253    /**
254     * gets the body length of the rolling stock
255     * 
256     * @return length in feet or meters
257     * @see #getTotalLength()
258     */
259    public String getLength() {
260        return _length;
261    }
262
263    public int getLengthInteger() {
264        try {
265            return Integer.parseInt(getLength());
266        } catch (NumberFormatException e) {
267            log.error("Rolling stock ({}) length ({}) is not valid ", this, getLength());
268        }
269        return 0;
270    }
271
272    /**
273     * Returns the length of the rolling stock including the couplers
274     *
275     * @return total length of the rolling stock in feet or meters
276     */
277    public int getTotalLength() {
278        return getLengthInteger() + RollingStock.COUPLERS;
279    }
280
281    public void setColorName(String color) {
282        String old = _color;
283        _color = color;
284        if (!old.equals(color)) {
285            setDirtyAndFirePropertyChange("rolling stock color", old, color); // NOI18N
286        }
287    }
288
289    public String getColorName() {
290        return _color;
291    }
292
293    /**
294     * @param weight rolling stock weight in ounces.
295     */
296    public void setWeight(String weight) {
297        String old = _weight;
298        _weight = weight;
299        if (!old.equals(weight)) {
300            setDirtyAndFirePropertyChange("rolling stock weight", old, weight); // NOI18N
301        }
302    }
303
304    public String getWeight() {
305        return _weight;
306    }
307
308    /**
309     * Sets the full scale weight in tons.
310     *
311     * @param weight full scale rolling stock weight in tons.
312     */
313    public void setWeightTons(String weight) {
314        String old = _weightTons;
315        _weightTons = weight;
316        if (!old.equals(weight)) {
317            setDirtyAndFirePropertyChange("rolling stock weight tons", old, weight); // NOI18N
318        }
319    }
320
321    public String getWeightTons() {
322        if (!_weightTons.equals(DEFAULT_WEIGHT)) {
323            return _weightTons;
324        }
325        // calculate the ton weight based on actual weight
326        double weight = 0;
327        try {
328            weight = Double.parseDouble(getWeight());
329        } catch (NumberFormatException e) {
330            log.trace("Weight not set for rolling stock ({})", this);
331        }
332        return Integer.toString((int) (weight * Setup.getScaleTonRatio()));
333    }
334
335    public int getAdjustedWeightTons() {
336        int weightTons = 0;
337        try {
338            // get loaded weight
339            weightTons = Integer.parseInt(getWeightTons());
340        } catch (NumberFormatException e) {
341            log.debug("Rolling stock ({}) weight not set", this);
342        }
343        return weightTons;
344    }
345
346    /**
347     * Set the date that the rolling stock was built. Use 4 digits for the year,
348     * or the format MM-YY where MM is the two digit month, and YY is the last
349     * two years if the rolling stock was built in the 1900s. Use MM-YYYY for
350     * units build after 1999.
351     *
352     * @param built The string built date.
353     */
354    public void setBuilt(String built) {
355        String old = _built;
356        _built = built;
357        if (!old.equals(built)) {
358            setDirtyAndFirePropertyChange("rolling stock built", old, built); // NOI18N
359        }
360    }
361
362    public String getBuilt() {
363        return _built;
364    }
365
366    /**
367     * @return location unknown symbol, out of service symbol, or none if car
368     *         okay
369     */
370    public String getStatus() {
371        return (isLocationUnknown() ? "<?> " : (isOutOfService() ? "<O> " : NONE)); // NOI18N
372    }
373
374    public Location getLocation() {
375        return _location;
376    }
377
378    /**
379     * Get rolling stock's location name
380     *
381     * @return empty string if rolling stock isn't on layout
382     */
383    public String getLocationName() {
384        if (getLocation() != null) {
385            return getLocation().getName();
386        }
387        return NONE;
388    }
389
390    public String getSplitLocationName() {
391        return TrainCommon.splitString(getLocationName());
392    }
393
394    /**
395     * Get rolling stock's location id
396     *
397     * @return empty string if rolling stock isn't on the layout
398     */
399    public String getLocationId() {
400        if (getLocation() != null) {
401            return getLocation().getId();
402        }
403        return NONE;
404    }
405
406    public Track getTrack() {
407        return _track;
408    }
409
410    /**
411     * Set the rolling stock's location and track. Doesn't do any checking and
412     * does not fire a property change. Used exclusively by the Router code. Use
413     * setLocation(Location, Track) instead.
414     *
415     * @param track to place the rolling stock on.
416     */
417    public void setTrack(Track track) {
418        if (track != null) {
419            _location = track.getLocation();
420        }
421        _track = track;
422    }
423
424    /**
425     * Get rolling stock's track name
426     *
427     * @return empty string if rolling stock isn't on a track
428     */
429    public String getTrackName() {
430        if (getTrack() != null) {
431            return getTrack().getName();
432        }
433        return NONE;
434    }
435
436    public String getSplitTrackName() {
437        return TrainCommon.splitString(getTrackName());
438    }
439
440    public String getTrackTypeName() {
441        if (getTrack() != null) {
442            return getTrack().getTrackTypeName();
443        }
444        return NONE;
445    }
446
447    /**
448     * Get rolling stock's track id
449     *
450     * @return empty string if rolling stock isn't on a track
451     */
452    public String getTrackId() {
453        if (getTrack() != null) {
454            return getTrack().getId();
455        }
456        return NONE;
457    }
458
459    /**
460     * Sets rolling stock location on the layout
461     *
462     * @param location The Location.
463     * @param track    (yard, spur, staging, or interchange track)
464     * @return "okay" if successful, "type" if the rolling stock's type isn't
465     *         acceptable, "road" if rolling stock road isn't acceptable, or
466     *         "length" if the rolling stock length didn't fit.
467     */
468    public String setLocation(Location location, Track track) {
469        return setLocation(location, track, !FORCE); // don't force
470    }
471
472    /**
473     * Sets rolling stock location on the layout
474     *
475     * @param location The Location.
476     * @param track    (yard, spur, staging, or interchange track)
477     * @param force    when true place rolling stock ignore track length, type,
478     *                 and road
479     * @return "okay" if successful, "type" if the rolling stock's type isn't
480     *         acceptable, "road" if rolling stock road isn't acceptable, or
481     *         "length" if the rolling stock length didn't fit.
482     */
483    public String setLocation(Location location, Track track, boolean force) {
484        Location oldLocation = getLocation();
485        Track oldTrack = getTrack();
486        // first determine if rolling stock can be move to the new location
487        if (!force && (oldLocation != location || oldTrack != track)) {
488            String status = testLocation(location, track);
489            if (!status.equals(Track.OKAY)) {
490                return status;
491            }
492        }
493        // now update
494        _location = location;
495        _track = track;
496
497        if (oldLocation != location || oldTrack != track) {
498            // update rolling stock location on layout, maybe this should be a property
499            // change?
500            // first remove rolling stock from existing location
501            if (oldLocation != null) {
502                oldLocation.deleteRS(this);
503                oldLocation.removePropertyChangeListener(this);
504                // if track is null, then rolling stock is in a train
505                if (oldTrack != null) {
506                    oldTrack.deleteRS(this);
507                    oldTrack.removePropertyChangeListener(this);
508                    // if there's a destination then pickup complete
509                    if (getDestination() != null) {
510                        oldLocation.deletePickupRS();
511                        oldTrack.deletePickupRS(this);
512                        // don't update rs's previous location if just re-staging
513                        if (!oldLocation.isStaging() ||
514                                location == null ||
515                                !location.isStaging() ||
516                                getTrain() != null &&
517                                        getTrain().getRoute() != null &&
518                                        getTrain().getRoute().size() > 2) {
519                            setLastLocationId(oldLocation.getId());
520                            setLastTrackId(oldTrack.getId());
521                        }
522                    }
523                }
524            }
525            if (getLocation() != null) {
526                getLocation().addRS(this);
527                // Need to know if location name changes so we can forward to listeners
528                getLocation().addPropertyChangeListener(this);
529            }
530            if (getTrack() != null) {
531                getTrack().addRS(this);
532                // Need to know if location name changes so we can forward to listeners
533                getTrack().addPropertyChangeListener(this);
534                // if there's a destination then there's a pick up
535                if (getDestination() != null) {
536                    getLocation().addPickupRS();
537                    getTrack().addPickupRS(this);
538                }
539            }
540            setDirtyAndFirePropertyChange(TRACK_CHANGED_PROPERTY, oldTrack, track);
541        }
542        return Track.OKAY;
543    }
544
545    /**
546     * Used to confirm that a track is associated with a location, and that
547     * rolling stock can be placed on track.
548     * 
549     * @param location The location
550     * @param track    The track, can be null
551     * @return OKAY if track exists at location and rolling stock can be placed
552     *         on track, ERROR_TRACK if track isn't at location, other if
553     *         rolling stock can't be placed on track.
554     */
555    public String testLocation(Location location, Track track) {
556        if (track == null) {
557            return Track.OKAY;
558        }
559        if (location != null && !location.isTrackAtLocation(track)) {
560            return ERROR_TRACK;
561        }
562        return track.isRollingStockAccepted(this);
563    }
564
565    /**
566     * Sets rolling stock destination on the layout
567     *
568     * @param destination The Location.
569     * @param track       (yard, spur, staging, or interchange track)
570     * @return "okay" if successful, "type" if the rolling stock's type isn't
571     *         acceptable, or "length" if the rolling stock length didn't fit.
572     */
573    public String setDestination(Location destination, Track track) {
574        return setDestination(destination, track, !RollingStock.FORCE);
575    }
576
577    /**
578     * Sets rolling stock destination on the layout
579     *
580     * @param destination The Location.
581     * @param track       (yard, spur, staging, or interchange track)
582     * @param force       when true ignore track length, type, and road when
583     *                    setting destination
584     * @return "okay" if successful, "type" if the rolling stock's type isn't
585     *         acceptable, or "length" if the rolling stock length didn't fit.
586     */
587    public String setDestination(Location destination, Track track, boolean force) {
588        // first determine if rolling stock can be move to the new destination
589        if (!force) {
590            String status = rsCheckDestination(destination, track);
591            if (!status.equals(Track.OKAY)) {
592                return status;
593            }
594        }
595        // now set the rolling stock destination!
596        Location oldDestination = getDestination();
597        _destination = destination;
598        Track oldTrack = getDestinationTrack();
599        _trackDestination = track;
600
601        if (oldDestination != destination || oldTrack != track) {
602            if (oldDestination != null) {
603                oldDestination.deleteDropRS();
604                oldDestination.removePropertyChangeListener(this);
605                // delete pick up in case destination is null
606                if (getLocation() != null && getTrack() != null) {
607                    getLocation().deletePickupRS();
608                    getTrack().deletePickupRS(this);
609                }
610            }
611            if (oldTrack != null) {
612                oldTrack.deleteDropRS(this);
613                oldTrack.removePropertyChangeListener(this);
614            }
615            if (getDestination() != null) {
616                getDestination().addDropRS();
617                if (getLocation() != null && getTrack() != null) {
618                    getLocation().addPickupRS();
619                    getTrack().addPickupRS(this);
620                }
621                // Need to know if destination name changes so we can forward to listeners
622                getDestination().addPropertyChangeListener(this);
623            }
624            if (getDestinationTrack() != null) {
625                getDestinationTrack().addDropRS(this);
626                // Need to know if destination name changes so we can forward to listeners
627                getDestinationTrack().addPropertyChangeListener(this);
628            } else {
629                // rolling stock has been terminated or reset
630                if (getTrain() != null && getTrain().getRoute() != null) {
631                    setLastRouteId(getTrain().getRoute().getId());
632                }
633                setRouteLocation(null);
634                setRouteDestination(null);
635            }
636            setDirtyAndFirePropertyChange(DESTINATION_TRACK_CHANGED_PROPERTY, oldTrack, track);
637        }
638        return Track.OKAY;
639    }
640
641    /**
642     * Used to check destination track to see if it will accept rolling stock
643     *
644     * @param destination The Location.
645     * @param track       The Track at destination.
646     * @return status OKAY, TYPE, ROAD, LENGTH, ERROR_TRACK
647     */
648    public String checkDestination(Location destination, Track track) {
649        return rsCheckDestination(destination, track);
650    }
651
652    private String rsCheckDestination(Location destination, Track track) {
653        // first perform a code check
654        if (destination != null && !destination.isTrackAtLocation(track)) {
655            return ERROR_TRACK;
656        }
657        if (destination != null && !destination.acceptsTypeName(getTypeName())) {
658            return Track.TYPE + " (" + getTypeName() + ")";
659        }
660        if (destination == null || track == null) {
661            return Track.OKAY;
662        }
663        return track.isRollingStockAccepted(this);
664    }
665
666    public Location getDestination() {
667        return _destination;
668    }
669
670    /**
671     * Sets rolling stock destination without reserving destination track space
672     * or drop count. Does not fire a property change. Used by car router to
673     * test destinations. Use setDestination(Location, Track) instead.
674     *
675     * @param destination for the rolling stock
676     */
677    public void setDestination(Location destination) {
678        _destination = destination;
679    }
680
681    public String getDestinationName() {
682        if (getDestination() != null) {
683            return getDestination().getName();
684        }
685        return NONE;
686    }
687
688    public String getSplitDestinationName() {
689        return TrainCommon.splitString(getDestinationName());
690    }
691
692    public String getDestinationId() {
693        if (getDestination() != null) {
694            return getDestination().getId();
695        }
696        return NONE;
697    }
698
699    /**
700     * Sets rolling stock destination track without reserving destination track
701     * space or drop count. Used by car router to test destinations. Does not
702     * fire a property change. Use setDestination(Location, Track) instead.
703     *
704     * @param track The Track for set out at destination.
705     */
706    public void setDestinationTrack(Track track) {
707        if (track != null) {
708            _destination = track.getLocation();
709        }
710        _trackDestination = track;
711    }
712
713    public Track getDestinationTrack() {
714        return _trackDestination;
715    }
716
717    public String getDestinationTrackName() {
718        if (getDestinationTrack() != null) {
719            return getDestinationTrack().getName();
720        }
721        return NONE;
722    }
723
724    public String getSplitDestinationTrackName() {
725        return TrainCommon.splitString(getDestinationTrackName());
726    }
727
728    public String getDestinationTrackId() {
729        if (getDestinationTrack() != null) {
730            return getDestinationTrack().getId();
731        }
732        return NONE;
733    }
734
735    public void setClone(boolean clone) {
736        boolean old = _clone;
737        _clone = clone;
738        if (!old == clone) {
739            setDirtyAndFirePropertyChange("clone", old, clone); // NOI18N
740        }
741    }
742
743    public boolean isClone() {
744        return _clone;
745    }
746
747    public void setCloneOrder(int number) {
748        _cloneOrder = number;
749    }
750
751    public int getCloneOrder() {
752        return _cloneOrder;
753    }
754
755    public void setDivision(Division division) {
756        Division old = _division;
757        _division = division;
758        if (old != _division) {
759            setDirtyAndFirePropertyChange("homeDivisionChange", old, division); // NOI18N
760        }
761    }
762
763    public Division getDivision() {
764        return _division;
765    }
766
767    public String getDivisionName() {
768        if (getDivision() != null) {
769            return getDivision().getName();
770        }
771        return NONE;
772    }
773
774    public String getDivisionId() {
775        if (getDivision() != null) {
776            return getDivision().getId();
777        }
778        return NONE;
779    }
780
781    /**
782     * Used to block cars from staging
783     *
784     * @param id The location id from where the car came from before going into
785     *           staging.
786     */
787    public void setLastLocationId(String id) {
788        _lastLocationId = id;
789    }
790
791    public String getLastLocationId() {
792        return _lastLocationId;
793    }
794
795    public String getLastLocationName() {
796        Location location = locationManager.getLocationById(getLastLocationId());
797        if (location != null) {
798            return location.getName();
799        }
800        return NONE;
801    }
802
803    public void setLastTrackId(String id) {
804        _lastTrackId = id;
805    }
806
807    public String getLastTrackId() {
808        return _lastTrackId;
809    }
810
811    public String getLastTrackName() {
812        Location location = locationManager.getLocationById(getLastLocationId());
813        if (location != null) {
814            Track track = location.getTrackById(getLastTrackId());
815            if (track != null) {
816                return track.getName();
817            }
818        }
819        return NONE;
820    }
821
822    public void setMoves(int moves) {
823        int old = _moves;
824        _moves = moves;
825        if (old != moves) {
826            setDirtyAndFirePropertyChange("rolling stock moves", old, moves); // NOI18N
827        }
828    }
829
830    public int getMoves() {
831        return _moves;
832    }
833
834    /**
835     * Sets the train that will service this rolling stock.
836     *
837     * @param train The Train.
838     */
839    public void setTrain(Train train) {
840        Train old = _train;
841        _train = train;
842        if (old != train) {
843            if (old != null) {
844                old.removePropertyChangeListener(this);
845            }
846            if (train != null) {
847                train.addPropertyChangeListener(this);
848            }
849            setDirtyAndFirePropertyChange(TRAIN_CHANGED_PROPERTY, old, train);
850        }
851    }
852
853    public Train getTrain() {
854        return _train;
855    }
856
857    public String getTrainName() {
858        if (getTrain() != null) {
859            return getTrain().getName();
860        }
861        return NONE;
862    }
863
864    /**
865     * Sets the last train that serviced this rolling stock.
866     *
867     * @param train The last Train.
868     */
869    public void setLastTrain(Train train) {
870        Train old = _lastTrain;
871        _lastTrain = train;
872        if (old != train) {
873            if (old != null) {
874                old.removePropertyChangeListener(this);
875            }
876            if (train != null) {
877                train.addPropertyChangeListener(this);
878            }
879            setDirtyAndFirePropertyChange(TRAIN_CHANGED_PROPERTY, old, train);
880        }
881    }
882
883    public Train getLastTrain() {
884        return _lastTrain;
885    }
886
887    public String getLastTrainName() {
888        if (getLastTrain() != null) {
889            return getLastTrain().getName();
890        }
891        return NONE;
892    }
893
894    /**
895     * Sets the location where the rolling stock will be picked up by the train.
896     *
897     * @param routeLocation the pick up location for this rolling stock.
898     */
899    public void setRouteLocation(RouteLocation routeLocation) {
900        // a couple of error checks before setting the route location
901        if (getLocation() == null && routeLocation != null) {
902            log.debug("WARNING rolling stock ({}) does not have an assigned location", this); // NOI18N
903        } else if (routeLocation != null &&
904                getLocation() != null &&
905                !routeLocation.getName().equals(getLocation().getName())) {
906            log.error("ERROR route location name({}) not equal to location name ({}) for rolling stock ({})",
907                    routeLocation.getName(), getLocation().getName(), this); // NOI18N
908        }
909        RouteLocation old = _routeLocation;
910        _routeLocation = routeLocation;
911        if (old != routeLocation) {
912            setDirtyAndFirePropertyChange(ROUTE_LOCATION_CHANGED_PROPERTY, old, routeLocation);
913        }
914    }
915
916    /**
917     * Where in a train's route this car resides
918     * 
919     * @return the location in a train's route
920     */
921    public RouteLocation getRouteLocation() {
922        return _routeLocation;
923    }
924
925    public String getRouteLocationId() {
926        if (getRouteLocation() != null) {
927            return getRouteLocation().getId();
928        }
929        return NONE;
930    }
931
932    /**
933     * Used to determine which train delivered a car to an interchange track.
934     * 
935     * @return the route id of the last train delivering this car.
936     */
937    public String getLastRouteId() {
938        return _routeId;
939    }
940
941    /**
942     * Sets the id of the route that was used to set out the rolling stock. Used
943     * to determine if the rolling stock can be pick ups from an interchange
944     * track.
945     *
946     * @param id The route id.
947     */
948    public void setLastRouteId(String id) {
949        _routeId = id;
950    }
951
952    public String getValue() {
953        return _value;
954    }
955
956    /**
957     * Sets the value (cost, price) for this rolling stock. Currently has
958     * nothing to do with operations. But nice to have.
959     *
960     * @param value a string representing what this item is worth.
961     */
962    public void setValue(String value) {
963        String old = _value;
964        _value = value;
965        if (!old.equals(value)) {
966            setDirtyAndFirePropertyChange("rolling stock value", old, value); // NOI18N
967        }
968    }
969
970    public String getRfid() {
971        return _rfid;
972    }
973
974    /**
975     * Sets the RFID for this rolling stock.
976     *
977     * @param id 12 character RFID string.
978     */
979    public void setRfid(String id) {
980        String old = _rfid;
981        if (id != null && !id.equals(old)) {
982            log.debug("Setting IdTag for {} to {}", this, id);
983            _rfid = id;
984            if (!id.equals(NONE)) {
985                try {
986                    IdTag tag = InstanceManager.getDefault(IdTagManager.class).provideIdTag(id);
987                    setIdTag(tag);
988                } catch (IllegalArgumentException e) {
989                    log.error("Exception recording tag {} - exception value {}", id, e.getMessage());
990                }
991            }
992            setDirtyAndFirePropertyChange("rolling stock rfid", old, id); // NOI18N
993        }
994    }
995
996    public IdTag getIdTag() {
997        return _tag;
998    }
999
1000    /**
1001     * Sets the id tag for this rolling stock. The id tag isn't saved, between
1002     * session but the tag label is saved as _rfid.
1003     *
1004     * @param tag the id tag
1005     */
1006    public void setIdTag(IdTag tag) {
1007        if (_tag != null) {
1008            _tag.removePropertyChangeListener(_tagListener);
1009        }
1010        _tag = tag;
1011        if (_tagListener == null) {
1012            // store the tag listener so we can reuse it and
1013            // dispose of it as necessary.
1014            _tagListener = new PropertyChangeListener() {
1015                @Override
1016                public void propertyChange(java.beans.PropertyChangeEvent e) {
1017                    if (e.getPropertyName().equals("whereLastSeen")) {
1018                        log.debug("Tag Reader Position update received for {}", this);
1019                        // update the position of this piece of rolling
1020                        // stock when its IdTag is seen, but only if
1021                        // the actual location changes.
1022                        if (e.getNewValue() != null) {
1023                            // first, check to see if this reader is
1024                            // associated with a track.
1025                            Track newTrack = locationManager.getTrackByReporter((jmri.Reporter) e.getNewValue());
1026                            if (newTrack != null) {
1027                                if (newTrack != getTrack()) {
1028                                    // set the car's location based on the track.
1029                                    setLocation(newTrack.getLocation(), newTrack);
1030                                    // also notify listeners that the last seen
1031                                    // location has changed.
1032                                    setDirtyAndFirePropertyChange("rolling stock whereLastSeen", _whereLastSeen,
1033                                            _whereLastSeen = newTrack.getLocation());
1034
1035                                }
1036                            } else {
1037                                // the reader isn't associated with a track,
1038                                Location newLocation = locationManager
1039                                        .getLocationByReporter((jmri.Reporter) e.getNewValue());
1040                                if (newLocation != getLocation()) {
1041                                    // we really should be able to set the
1042                                    // location where we last saw the tag:
1043                                    // setLocation(newLocation,null);
1044                                    // for now, notify listeners that the
1045                                    // location changed.
1046                                    setDirtyAndFirePropertyChange("rolling stock whereLastSeen", _whereLastSeen,
1047                                            _whereLastSeen = newLocation);
1048
1049                                }
1050                            }
1051                        }
1052                    }
1053                    if (e.getPropertyName().equals("whenLastSeen")) {
1054                        log.debug("Tag Reader Time at Location update received for {}", this);
1055                        // update the time when this car was last moved
1056                        // stock when its IdTag is seen.
1057                        if (e.getNewValue() != null) {
1058                            LocalDateTime newDate = ((LocalDateTime) e.getNewValue());
1059                            setLastDate(newDate);
1060                            // and notify listeners when last seen was updated.
1061                            setDirtyAndFirePropertyChange("rolling stock whenLastSeen", _whenLastSeen,
1062                                    _whenLastSeen = newDate);
1063                        }
1064                    }
1065                }
1066            };
1067        }
1068        if (_tag != null) {
1069            _tag.addPropertyChangeListener(_tagListener);
1070            setRfid(_tag.getSystemName());
1071        } else {
1072            setRfid(NONE);
1073        }
1074        // initilize _whenLastSeen and _whereLastSeen for property
1075        // change notification.
1076        _whereLastSeen = getWhereLastSeen();
1077        _whenLastSeen = getWhenLastSeen();
1078    }
1079
1080    public String getWhereLastSeenName() {
1081        if (getWhereLastSeen() != null) {
1082            return getWhereLastSeen().getName();
1083        }
1084        return NONE;
1085    }
1086
1087    public Location getWhereLastSeen() {
1088        if (_tag == null) {
1089            return null;
1090        }
1091        jmri.Reporter r = _tag.getWhereLastSeen();
1092        Track t = locationManager.getTrackByReporter(r);
1093        if (t != null) {
1094            return t.getLocation();
1095        }
1096        // the reader isn't associated with a track, return
1097        // the location it is associated with, which might be null.
1098        return locationManager.getLocationByReporter(r);
1099    }
1100
1101    public Track getTrackLastSeen() {
1102        if (_tag == null) {
1103            // there isn't a tag associated with this piece of rolling stock.
1104            return null;
1105        }
1106        jmri.Reporter r = _tag.getWhereLastSeen();
1107        if (r == null) {
1108            // there is a tag associated with this piece
1109            // of rolling stock, but no reporter has seen it (or it was reset).
1110            return null;
1111        }
1112        // this return value will be null, if there isn't an associated track
1113        // for the last reporter.
1114        return locationManager.getTrackByReporter(r);
1115    }
1116
1117    public String getTrackLastSeenName() {
1118        // let getTrackLastSeen() find the track, if it exists.
1119        Track t = getTrackLastSeen();
1120        if (t != null) {
1121            // if there is a track, return the name.
1122            return t.getName();
1123        }
1124        // otherwise, there is no track to return the name of.
1125        return NONE;
1126    }
1127
1128    public LocalDateTime getWhenLastSeen() {
1129        if (_tag != null) {
1130            Date date = _tag.getWhenLastSeen();
1131            if (date != null) {
1132                return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
1133            }
1134        }
1135        return null; // never seen, so no date.
1136    }
1137
1138    /**
1139     * Provides the last date when this rolling stock was moved, or was reset
1140     * from a built train, as a string.
1141     *
1142     * @return date
1143     */
1144    public String getWhenLastSeenDate() {
1145        if (getWhenLastSeen() == null) {
1146            return NONE; // return an empty string for the default date.
1147        }
1148        SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N
1149        return format.format(getWhenLastSeen());
1150    }
1151
1152    /**
1153     * Provides the last date when this rolling stock was moved
1154     *
1155     * @return String yyyy/MM/dd HH:mm:ss
1156     */
1157    public String getSortDate() {
1158        if (_lastDate.equals(epoch)) {
1159            return NONE; // return an empty string for the default date.
1160        }
1161        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); // NOI18N
1162        return _lastDate.format(format);
1163    }
1164
1165    public static final int DATE_TIME_LENGTH = 19;
1166
1167    /**
1168     * Provides the last date when this rolling stock was moved
1169     *
1170     * @return String MM/dd/yyyy HH:mm:ss
1171     */
1172    public String getLastDate() {
1173        if (_lastDate.equals(epoch)) {
1174            return NONE; // return an empty string for the default date.
1175        }
1176        DateTimeFormatter format = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"); // NOI18N
1177        return _lastDate.format(format);
1178    }
1179
1180    /**
1181     * Sets the last date when this rolling stock was moved
1182     *
1183     * @param date The Date when this rolling stock was last moved.
1184     */
1185    public void setLastDate(LocalDateTime date) {
1186        LocalDateTime old = _lastDate;
1187        _lastDate = date;
1188        if (!old.equals(_lastDate)) {
1189            setDirtyAndFirePropertyChange("rolling stock date", old, date); // NOI18N
1190        }
1191    }
1192
1193    /**
1194     * Provides the last date when this rolling stock was moved
1195     *
1196     * @return date
1197     */
1198    public LocalDateTime getLastMoveDate() {
1199        return _lastDate;
1200    }
1201
1202    /**
1203     * Sets the last date when this rolling stock was moved. This method is used
1204     * only for loading data from a file. Use setLastDate(Date) instead.
1205     *
1206     * @param date MM/dd/yyyy HH:mm:ss, MM/dd/yyyy hh:mm a, or MM/dd/yyyy HH:mm
1207     */
1208    public void setLastDate(String date) {
1209        LocalDateTime d = TrainCommon.convertStringToLocalDateTime(date);
1210        if (d != null) {
1211            _lastDate = d;
1212        }
1213    }
1214
1215    public void setBlocking(int number) {
1216        int old = _blocking;
1217        _blocking = number;
1218        if (old != number) {
1219            setDirtyAndFirePropertyChange("rolling stock blocking changed", old, number); // NOI18N
1220        }
1221    }
1222
1223    public int getBlocking() {
1224        return _blocking;
1225    }
1226
1227    /**
1228     * Set where in a train's route this rolling stock will be set out.
1229     *
1230     * @param routeDestination the location where the rolling stock is to leave
1231     *                         the train.
1232     */
1233    public void setRouteDestination(RouteLocation routeDestination) {
1234        if (routeDestination != null &&
1235                getDestination() != null &&
1236                !routeDestination.getName().equals(getDestination().getName())) {
1237            log.debug("WARNING route destination name ({}) not equal to destination name ({}) for rolling stock ({})",
1238                    routeDestination.getName(), getDestination().getName(), this); // NOI18N
1239        }
1240        RouteLocation old = _routeDestination;
1241        _routeDestination = routeDestination;
1242        if (old != routeDestination) {
1243            setDirtyAndFirePropertyChange(ROUTE_DESTINATION_CHANGED_PROPERTY, old, routeDestination);
1244        }
1245    }
1246
1247    public RouteLocation getRouteDestination() {
1248        return _routeDestination;
1249    }
1250
1251    public String getRouteDestinationId() {
1252        if (getRouteDestination() != null) {
1253            return getRouteDestination().getId();
1254        }
1255        return NONE;
1256    }
1257
1258    public void setRouteDestinationTiming(RouteLocation routeDestination) {
1259        _routeDestinationTiming = routeDestination;
1260    }
1261
1262    public RouteLocation getRouteDestinationTiming() {
1263        return _routeDestinationTiming;
1264    }
1265
1266    public void setOwnerName(String owner) {
1267        String old = _owner;
1268        _owner = owner;
1269        if (!old.equals(owner)) {
1270            setDirtyAndFirePropertyChange("rolling stock owner", old, owner); // NOI18N
1271        }
1272    }
1273
1274    public String getOwnerName() {
1275        return _owner;
1276    }
1277
1278    /**
1279     * Set the rolling stock location as unknown.
1280     *
1281     * @param unknown when true, the rolling stock location is unknown.
1282     */
1283    public void setLocationUnknown(boolean unknown) {
1284        boolean old = _locationUnknown;
1285        _locationUnknown = unknown;
1286        if (!old == unknown) {
1287            setDirtyAndFirePropertyChange("car location known", old, unknown); // NOI18N
1288        }
1289    }
1290
1291    /**
1292     * @return true when car's location is unknown
1293     */
1294    public boolean isLocationUnknown() {
1295        return _locationUnknown;
1296    }
1297
1298    /**
1299     * Sets the rolling stock service state. When true, rolling stock is out of
1300     * service. Normal state is false, the rolling stock is in service and
1301     * available.
1302     *
1303     * @param outOfService when true, out of service
1304     */
1305    public void setOutOfService(boolean outOfService) {
1306        boolean old = _outOfService;
1307        _outOfService = outOfService;
1308        if (!old == outOfService) {
1309            setDirtyAndFirePropertyChange("car out of service", old, outOfService); // NOI18N
1310        }
1311    }
1312
1313    /**
1314     * @return true when rolling stock is out of service
1315     */
1316    public boolean isOutOfService() {
1317        return _outOfService;
1318    }
1319
1320    public void setSelected(boolean selected) {
1321        boolean old = _selected;
1322        _selected = selected;
1323        if (!old == selected) {
1324            setDirtyAndFirePropertyChange("selected", old, selected); // NOI18N
1325        }
1326    }
1327
1328    /**
1329     * @return true when rolling stock is selected
1330     */
1331    public boolean isSelected() {
1332        return _selected;
1333    }
1334
1335    public void setComment(String comment) {
1336        String old = _comment;
1337        _comment = comment;
1338        if (!old.equals(comment)) {
1339            setDirtyAndFirePropertyChange(COMMENT_CHANGED_PROPERTY, old, comment);
1340        }
1341    }
1342
1343    public String getComment() {
1344        return _comment;
1345    }
1346
1347    public void setPickupTime(String time) {
1348        String old = _pickupTime;
1349        _pickupTime = time;
1350        setDirtyAndFirePropertyChange("Pickup Time Changed", old, time); // NOI18N
1351    }
1352
1353    public String getPickupTime() {
1354        if (getTrain() != null) {
1355            return _pickupTime;
1356        }
1357        return NONE;
1358    }
1359
1360    public void setSetoutTime(String time) {
1361        String old = _setoutTime;
1362        _setoutTime = time;
1363        setDirtyAndFirePropertyChange("Setout Time Changed", old, time); // NOI18N
1364    }
1365
1366    public String getSetoutTime() {
1367        if (getTrain() != null && getTrain().isBuilding() && getRouteDestination() != null) {
1368            _setoutTime = getTrain().getExpectedArrivalTime(getRouteDestination(), true);
1369        }
1370        if (getTrain() != null) {
1371            return _setoutTime;
1372        }
1373        return NONE;
1374    }
1375
1376    /**
1377     * Used when building Manifest
1378     * @param order the order rolling stock is added to train
1379     */
1380    public void setOrder(String order) {
1381        _order = order;
1382    }
1383    
1384    public String getOrder() {
1385        return _order;
1386    }
1387
1388    protected void moveRollingStock(RouteLocation current, RouteLocation next) {
1389        if (current == getRouteLocation()) {
1390            setLastDate(LocalDateTime.now());
1391            // Arriving at destination?
1392            if (getRouteLocation() == getRouteDestination() || next == null) {
1393                if (getRouteLocation() == getRouteDestination()) {
1394                    log.debug("Rolling stock ({}) has arrived at destination ({})", this, getDestination());
1395                } else {
1396                    log.error("Rolling stock ({}) has a null route location for next", this); // NOI18N
1397                }
1398                setLocation(getDestination(), getDestinationTrack(), RollingStock.FORCE); // force RS to destination
1399                setDestination(null, null); // this also clears the route locations
1400                setLastTrain(getTrain()); // save the last train moving this rs
1401                setTrain(null); // this must come after setDestination (route id is set)
1402                setMoves(getMoves() + 1); // bump count
1403                setPickupTime(NONE);
1404            } else {
1405                log.debug("Rolling stock ({}) is in train ({}) leaves location ({}) destination ({})", this,
1406                        getTrainName(), current.getName(), next.getName());
1407                setLocation(next.getLocation(), null, RollingStock.FORCE); // force RS to location
1408                setRouteLocation(next);
1409            }
1410        }
1411    }
1412
1413    public void reset() {
1414        setPickupTime(NONE);
1415        // the order of the next two instructions is important, otherwise rs will have
1416        // train's route id
1417        setTrain(null);
1418        setDestination(null, null);
1419    }
1420
1421    /*
1422     * Clone has been reset and is in the process of being destroyed, move
1423     * original car back and restore the car's settings.
1424     */
1425    protected void destroyCloneReset(RollingStock rs) {
1426        rs.setLocation(getLocation(), getTrack(), RollingStock.FORCE);
1427        rs.setRouteDestination(null);
1428        rs.setLastTrain(getLastTrain());
1429        rs.setLastRouteId(getLastRouteId());
1430        rs.setLastDate(getLastDate());
1431        rs.setLastLocationId(getLastLocationId());
1432        rs.setLastTrackId(getLastTrackId());
1433        rs.setMoves(getMoves());
1434    }
1435
1436    /**
1437     * Remove rolling stock. Releases all listeners.
1438     */
1439    public void dispose() {
1440        setTrain(null);
1441        setDestination(null, null);
1442        setLocation(null, null);
1443        removePropertyChangeListeners();
1444        if (getIdTag() != null) {
1445            getIdTag().removePropertyChangeListener(_tagListener);
1446        }
1447    }
1448
1449    /**
1450     * Construct this Entry from XML.
1451     *
1452     * @param e RollingStock XML element
1453     */
1454    public RollingStock(org.jdom2.Element e) {
1455        this();
1456        org.jdom2.Attribute a;
1457        if ((a = e.getAttribute(Xml.ID)) != null) {
1458            _id = a.getValue();
1459        } else {
1460            log.warn("no id attribute in rolling stock element when reading operations");
1461        }
1462        if ((a = e.getAttribute(Xml.ORDER)) != null) {
1463            _order = a.getValue();
1464        }
1465        if ((a = e.getAttribute(Xml.ROAD_NUMBER)) != null) {
1466            _number = a.getValue();
1467        }
1468        if ((a = e.getAttribute(Xml.ROAD_NAME)) != null) {
1469            _road = a.getValue();
1470        }
1471        if (_id == null || !_id.equals(createId(_road, _number))) {
1472            _id = createId(_road, _number);
1473        }
1474        if ((a = e.getAttribute(Xml.TYPE)) != null) {
1475            _type = a.getValue();
1476        }
1477        if ((a = e.getAttribute(Xml.LENGTH)) != null) {
1478            _length = a.getValue();
1479        }
1480        if ((a = e.getAttribute(Xml.COLOR)) != null) {
1481            _color = a.getValue();
1482        }
1483        if ((a = e.getAttribute(Xml.WEIGHT)) != null) {
1484            _weight = a.getValue();
1485        }
1486        if ((a = e.getAttribute(Xml.WEIGHT_TONS)) != null) {
1487            _weightTons = a.getValue();
1488        }
1489        if ((a = e.getAttribute(Xml.BUILT)) != null) {
1490            _built = a.getValue();
1491        }
1492        if ((a = e.getAttribute(Xml.CLONE)) != null) {
1493            _clone = a.getValue().equals(Xml.TRUE);
1494        }
1495        Location location = null;
1496        Track track = null;
1497        if ((a = e.getAttribute(Xml.LOCATION_ID)) != null) {
1498            location = locationManager.getLocationById(a.getValue());
1499        }
1500        if ((a = e.getAttribute(Xml.SEC_LOCATION_ID)) != null && location != null) {
1501            track = location.getTrackById(a.getValue());
1502        }
1503        setLocation(location, track, RollingStock.FORCE); // force location
1504
1505        Location destination = null;
1506        track = null;
1507        if ((a = e.getAttribute(Xml.DESTINATION_ID)) != null) {
1508            destination = locationManager.getLocationById(a.getValue());
1509        }
1510        if ((a = e.getAttribute(Xml.SEC_DESTINATION_ID)) != null && destination != null) {
1511            track = destination.getTrackById(a.getValue());
1512        }
1513        setDestination(destination, track, RollingStock.FORCE); // force destination
1514
1515        if ((a = e.getAttribute(Xml.DIVISION_ID)) != null) {
1516            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1517        }
1518        // make xml error "DivisionId" backward compatible
1519        if ((a = e.getAttribute(Xml.DIVISION_ID_ERROR)) != null) {
1520            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1521        }
1522        if ((a = e.getAttribute(Xml.MOVES)) != null) {
1523            try {
1524                _moves = Integer.parseInt(a.getValue());
1525            } catch (NumberFormatException nfe) {
1526                log.error("Move count ({}) for rollingstock ({}) isn't a valid number!", a.getValue(), toString());
1527            }
1528        }
1529        if ((a = e.getAttribute(Xml.LAST_LOCATION_ID)) != null) {
1530            _lastLocationId = a.getValue();
1531        }
1532        if ((a = e.getAttribute(Xml.LAST_TRACK_ID)) != null) {
1533            _lastTrackId = a.getValue();
1534        }
1535        if ((a = e.getAttribute(Xml.TRAIN_ID)) != null) {
1536            setTrain(InstanceManager.getDefault(TrainManager.class).getTrainById(a.getValue()));
1537        } else if ((a = e.getAttribute(Xml.TRAIN)) != null) {
1538            setTrain(InstanceManager.getDefault(TrainManager.class).getTrainByName(a.getValue()));
1539        }
1540        if (getTrain() != null &&
1541                getTrain().getRoute() != null &&
1542                (a = e.getAttribute(Xml.ROUTE_LOCATION_ID)) != null) {
1543            _routeLocation = getTrain().getRoute().getRouteLocationById(a.getValue());
1544            if ((a = e.getAttribute(Xml.ROUTE_DESTINATION_ID)) != null) {
1545                _routeDestination = getTrain().getRoute().getRouteLocationById(a.getValue());
1546            }
1547        }
1548        if ((a = e.getAttribute(Xml.LAST_TRAIN_ID)) != null) {
1549            setLastTrain(InstanceManager.getDefault(TrainManager.class).getTrainById(a.getValue()));
1550        }
1551        if ((a = e.getAttribute(Xml.LAST_ROUTE_ID)) != null) {
1552            _routeId = a.getValue();
1553        }
1554        if ((a = e.getAttribute(Xml.OWNER)) != null) {
1555            _owner = a.getValue();
1556        }
1557        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
1558            _comment = a.getValue();
1559        }
1560        if ((a = e.getAttribute(Xml.VALUE)) != null) {
1561            _value = a.getValue();
1562        }
1563        if ((a = e.getAttribute(Xml.RFID)) != null) {
1564            setRfid(a.getValue());
1565        }
1566        if ((a = e.getAttribute(Xml.LOC_UNKNOWN)) != null) {
1567            _locationUnknown = a.getValue().equals(Xml.TRUE);
1568        }
1569        if ((a = e.getAttribute(Xml.OUT_OF_SERVICE)) != null) {
1570            _outOfService = a.getValue().equals(Xml.TRUE);
1571        }
1572        if ((a = e.getAttribute(Xml.SELECTED)) != null) {
1573            _selected = a.getValue().equals(Xml.TRUE);
1574        }
1575        if ((a = e.getAttribute(Xml.DATE)) != null) {
1576            setLastDate(a.getValue()); // uses the setLastDate(String) method.
1577        }
1578        if ((a = e.getAttribute(Xml.PICKUP_TIME)) != null) {
1579            _pickupTime = a.getValue();
1580        }
1581        if ((a = e.getAttribute(Xml.SETOUT_TIME)) != null) {
1582            _setoutTime = a.getValue();
1583        }
1584        if ((a = e.getAttribute(Xml.BLOCKING)) != null) {
1585            try {
1586                _blocking = Integer.parseInt(a.getValue());
1587            } catch (NumberFormatException nfe) {
1588                log.error("Blocking ({}) for rollingstock ({}) isn't a valid number!", a.getValue(), toString());
1589            }
1590        }
1591        // check for rolling stock without a track assignment
1592        if (getLocation() != null && getTrack() == null && getTrain() == null) {
1593            log.warn("Rollingstock ({}) at ({}) doesn't have a track assignment", this, getLocationName());
1594        }
1595        addPropertyChangeListeners();
1596    }
1597
1598    /**
1599     * Add XML elements to represent this Entry.
1600     *
1601     * @param e Element for car or engine store.
1602     * @return Contents in a JDOM Element
1603     */
1604    protected org.jdom2.Element store(org.jdom2.Element e) {
1605        e.setAttribute(Xml.ID, getId());
1606        e.setAttribute(Xml.ROAD_NAME, getRoadName());
1607        e.setAttribute(Xml.ROAD_NUMBER, getNumber());
1608        e.setAttribute(Xml.TYPE, getTypeName());
1609        e.setAttribute(Xml.LENGTH, getLength());
1610        if (!getColorName().equals(NONE)) {
1611            e.setAttribute(Xml.COLOR, getColorName());
1612        }
1613        if (!getWeight().equals(DEFAULT_WEIGHT)) {
1614            e.setAttribute(Xml.WEIGHT, getWeight());
1615        }
1616        if (!getWeightTons().equals(NONE)) {
1617            e.setAttribute(Xml.WEIGHT_TONS, getWeightTons());
1618        }
1619        if (!getBuilt().equals(NONE)) {
1620            e.setAttribute(Xml.BUILT, getBuilt());
1621        }
1622        if (!getLocationId().equals(NONE)) {
1623            e.setAttribute(Xml.LOCATION_ID, getLocationId());
1624        }
1625        if (!getRouteLocationId().equals(NONE)) {
1626            e.setAttribute(Xml.ROUTE_LOCATION_ID, getRouteLocationId());
1627        }
1628        if (!getTrackId().equals(NONE)) {
1629            e.setAttribute(Xml.SEC_LOCATION_ID, getTrackId());
1630        }
1631        if (!getDestinationId().equals(NONE)) {
1632            e.setAttribute(Xml.DESTINATION_ID, getDestinationId());
1633        }
1634        if (!getRouteDestinationId().equals(NONE)) {
1635            e.setAttribute(Xml.ROUTE_DESTINATION_ID, getRouteDestinationId());
1636        }
1637        if (!getDestinationTrackId().equals(NONE)) {
1638            e.setAttribute(Xml.SEC_DESTINATION_ID, getDestinationTrackId());
1639        }
1640        if (!getDivisionId().equals(NONE)) {
1641            e.setAttribute(Xml.DIVISION_ID, getDivisionId());
1642        }
1643        if (!getLastRouteId().equals(NONE)) {
1644            e.setAttribute(Xml.LAST_ROUTE_ID, getLastRouteId());
1645        }
1646        if (isClone()) {
1647            e.setAttribute(Xml.CLONE, isClone() ? Xml.TRUE : Xml.FALSE);
1648        }
1649        e.setAttribute(Xml.MOVES, Integer.toString(getMoves()));
1650        e.setAttribute(Xml.DATE, getLastDate());
1651        e.setAttribute(Xml.SELECTED, isSelected() ? Xml.TRUE : Xml.FALSE);
1652        if (!getLastLocationId().equals(LOCATION_UNKNOWN)) {
1653            e.setAttribute(Xml.LAST_LOCATION_ID, getLastLocationId());
1654        }
1655        if (!getLastTrackId().equals(LOCATION_UNKNOWN)) {
1656            e.setAttribute(Xml.LAST_TRACK_ID, getLastTrackId());
1657        }
1658        if (!getTrainName().equals(NONE)) {
1659            e.setAttribute(Xml.TRAIN, getTrainName());
1660            e.setAttribute(Xml.TRAIN_ID, getTrain().getId());
1661            e.setAttribute(Xml.ORDER, getOrder());
1662        }
1663        if (!getLastTrainName().equals(NONE)) {
1664            e.setAttribute(Xml.LAST_TRAIN, getLastTrainName());
1665            e.setAttribute(Xml.LAST_TRAIN_ID, getLastTrain().getId());
1666        }
1667        if (!getOwnerName().equals(NONE)) {
1668            e.setAttribute(Xml.OWNER, getOwnerName());
1669        }
1670        if (!getValue().equals(NONE)) {
1671            e.setAttribute(Xml.VALUE, getValue());
1672        }
1673        if (!getRfid().equals(NONE)) {
1674            e.setAttribute(Xml.RFID, getRfid());
1675        }
1676        if (isLocationUnknown()) {
1677            e.setAttribute(Xml.LOC_UNKNOWN, isLocationUnknown() ? Xml.TRUE : Xml.FALSE);
1678        }
1679        if (isOutOfService()) {
1680            e.setAttribute(Xml.OUT_OF_SERVICE, isOutOfService() ? Xml.TRUE : Xml.FALSE);
1681        }
1682        if (!getPickupTime().equals(NONE)) {
1683            e.setAttribute(Xml.PICKUP_TIME, getPickupTime());
1684        }
1685        if (!getSetoutTime().equals(NONE)) {
1686            e.setAttribute(Xml.SETOUT_TIME, getSetoutTime());
1687        }
1688        if (getBlocking() != 0) {
1689            e.setAttribute(Xml.BLOCKING, Integer.toString(getBlocking()));
1690        }
1691        if (!getComment().equals(NONE)) {
1692            e.setAttribute(Xml.COMMENT, getComment());
1693        }
1694        return e;
1695    }
1696
1697    protected void addPropertyChangeListeners() {
1698        InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
1699        InstanceManager.getDefault(CarOwners.class).addPropertyChangeListener(this);
1700        InstanceManager.getDefault(CarColors.class).addPropertyChangeListener(this);
1701    }
1702    
1703    protected void removePropertyChangeListeners() {
1704        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
1705        InstanceManager.getDefault(CarOwners.class).removePropertyChangeListener(this);
1706        InstanceManager.getDefault(CarColors.class).removePropertyChangeListener(this);
1707    }
1708
1709    // rolling stock listens for changes in a location name or if a location is
1710    // deleted
1711    @Override
1712    public void propertyChange(PropertyChangeEvent e) {
1713        // log.debug("Property change for rolling stock: " + toString()+ " property
1714        // name: "
1715        // +e.getPropertyName()+ " old: "+e.getOldValue()+ " new: "+e.getNewValue());
1716        // notify if track or location name changes
1717        if (e.getPropertyName().equals(Location.NAME_CHANGED_PROPERTY)) {
1718            log.debug("Property change for rolling stock: ({}) property name: ({}) old: ({}) new: ({})", this,
1719                    e.getPropertyName(), e.getOldValue(), e.getNewValue());
1720            setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
1721        }
1722        if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) {
1723            if (e.getSource() == getLocation()) {
1724                log.debug("delete location for rolling stock: ({})", this);
1725                setLocation(null, null);
1726            }
1727            if (e.getSource() == getDestination()) {
1728                log.debug("delete destination for rolling stock: ({})", this);
1729                setDestination(null, null);
1730            }
1731        }
1732        if (e.getPropertyName().equals(Track.DISPOSE_CHANGED_PROPERTY)) {
1733            if (e.getSource() == getTrack()) {
1734                log.debug("delete location for rolling stock: ({})", this);
1735                setLocation(getLocation(), null);
1736            }
1737            if (e.getSource() == getDestinationTrack()) {
1738                log.debug("delete destination for rolling stock: ({})", this);
1739                setDestination(getDestination(), null);
1740            }
1741        }
1742        if (e.getPropertyName().equals(Train.DISPOSE_CHANGED_PROPERTY) && e.getSource() == getTrain()) {
1743            log.debug("delete train for rolling stock: ({})", this);
1744            setTrain(null);
1745        }
1746        if (e.getPropertyName().equals(Train.TRAIN_LOCATION_CHANGED_PROPERTY) && e.getSource() == getTrain()) {
1747            log.debug("Rolling stock ({}) is serviced by train ({})", this, getTrainName());
1748            moveRollingStock((RouteLocation) e.getOldValue(), (RouteLocation) e.getNewValue());
1749        }
1750        if (e.getPropertyName().equals(Train.STATUS_CHANGED_PROPERTY) &&
1751                e.getNewValue().equals(Train.TRAIN_RESET) &&
1752                e.getSource() == getTrain()) {
1753            log.debug("Rolling stock ({}) is removed from train ({}) by reset", this, getTrainName()); // NOI18N
1754            reset();
1755        }
1756        if (e.getPropertyName().equals(Train.NAME_CHANGED_PROPERTY)) {
1757            setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
1758        }
1759        if (e.getPropertyName().equals(CarRoads.CARROADS_NAME_CHANGED_PROPERTY)) {
1760            if (e.getOldValue().equals(getRoadName())) {
1761                log.debug("Rolling stock ({}) sees road name change from ({}) to ({})", this, e.getOldValue(),
1762                        e.getNewValue()); // NOI18N
1763                if (e.getNewValue() != null) {
1764                    setRoadName((String) e.getNewValue());
1765                }
1766            }
1767        }
1768        if (e.getPropertyName().equals(CarOwners.CAROWNERS_NAME_CHANGED_PROPERTY)) {
1769            if (e.getOldValue().equals(getOwnerName())) {
1770                log.debug("Rolling stock ({}) sees owner name change from ({}) to ({})", this, e.getOldValue(),
1771                        e.getNewValue()); // NOI18N
1772                setOwnerName((String) e.getNewValue());
1773            }
1774        }
1775        if (e.getPropertyName().equals(CarColors.CARCOLORS_NAME_CHANGED_PROPERTY)) {
1776            if (e.getOldValue().equals(getColorName())) {
1777                log.debug("Rolling stock ({}) sees color name change from ({}) to ({})", this, e.getOldValue(),
1778                        e.getNewValue()); // NOI18N
1779                setColorName((String) e.getNewValue());
1780            }
1781        }
1782    }
1783
1784    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1785        firePropertyChange(p, old, n);
1786    }
1787
1788    private static final Logger log = LoggerFactory.getLogger(RollingStock.class);
1789
1790}