001package jmri.jmrit.operations.rollingstock.cars;
002
003import java.beans.PropertyChangeEvent;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008import jmri.InstanceManager;
009import jmri.jmrit.operations.locations.*;
010import jmri.jmrit.operations.locations.schedules.Schedule;
011import jmri.jmrit.operations.locations.schedules.ScheduleItem;
012import jmri.jmrit.operations.rollingstock.RollingStock;
013import jmri.jmrit.operations.routes.RouteLocation;
014import jmri.jmrit.operations.trains.TrainCommon;
015import jmri.jmrit.operations.trains.schedules.TrainSchedule;
016import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
017
018/**
019 * Represents a car on the layout
020 *
021 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2010, 2012, 2013, 2014,
022 *         2015, 2023
023 */
024public class Car extends RollingStock {
025
026    CarLoads carLoads = InstanceManager.getDefault(CarLoads.class);
027
028    protected boolean _passenger = false;
029    protected boolean _hazardous = false;
030    protected boolean _caboose = false;
031    protected boolean _fred = false;
032    protected boolean _utility = false;
033    protected boolean _loadGeneratedByStaging = false;
034    protected Kernel _kernel = null;
035    protected String _loadName = carLoads.getDefaultEmptyName();
036    protected int _wait = 0;
037
038    protected Location _rweDestination = null; // return when empty destination
039    protected Track _rweDestTrack = null; // return when empty track
040    protected String _rweLoadName = carLoads.getDefaultEmptyName();
041
042    protected Location _rwlDestination = null; // return when loaded destination
043    protected Track _rwlDestTrack = null; // return when loaded track
044    protected String _rwlLoadName = carLoads.getDefaultLoadName();
045
046    // schedule items
047    protected String _scheduleId = NONE; // the schedule id assigned to this car
048    protected String _nextLoadName = NONE; // next load by schedule
049    protected Location _finalDestination = null; 
050    protected Track _finalDestTrack = null; // final track by schedule or router
051    protected Location _previousFinalDestination = null;
052    protected Track _previousFinalDestTrack = null;
053    protected String _previousScheduleId = NONE;
054    protected String _pickupScheduleId = NONE;
055
056    public static final String EXTENSION_REGEX = " ";
057    public static final String CABOOSE_EXTENSION = Bundle.getMessage("(C)");
058    public static final String FRED_EXTENSION = Bundle.getMessage("(F)");
059    public static final String PASSENGER_EXTENSION = Bundle.getMessage("(P)");
060    public static final String UTILITY_EXTENSION = Bundle.getMessage("(U)");
061    public static final String HAZARDOUS_EXTENSION = Bundle.getMessage("(H)");
062
063    public static final String LOAD_CHANGED_PROPERTY = "Car load changed"; // NOI18N
064    public static final String RWE_LOAD_CHANGED_PROPERTY = "Car RWE load changed"; // NOI18N
065    public static final String RWL_LOAD_CHANGED_PROPERTY = "Car RWL load changed"; // NOI18N
066    public static final String WAIT_CHANGED_PROPERTY = "Car wait changed"; // NOI18N
067    public static final String FINAL_DESTINATION_CHANGED_PROPERTY = "Car final destination changed"; // NOI18N
068    public static final String FINAL_DESTINATION_TRACK_CHANGED_PROPERTY = "Car final destination track changed"; // NOI18N
069    public static final String RETURN_WHEN_EMPTY_CHANGED_PROPERTY = "Car return when empty changed"; // NOI18N
070    public static final String RETURN_WHEN_LOADED_CHANGED_PROPERTY = "Car return when loaded changed"; // NOI18N
071    public static final String SCHEDULE_ID_CHANGED_PROPERTY = "car schedule id changed"; // NOI18N
072    public static final String KERNEL_NAME_CHANGED_PROPERTY = "kernel name changed"; // NOI18N
073
074    public Car() {
075        super();
076        loaded = true;
077    }
078
079    public Car(String road, String number) {
080        super(road, number);
081        loaded = true;
082        log.debug("New car ({} {})", road, number);
083        addPropertyChangeListeners();
084    }
085
086    public Car copy() {
087        Car car = new Car();
088        car.setBuilt(getBuilt());
089        car.setColor(getColor());
090        car.setLength(getLength());
091        car.setLoadName(getLoadName());
092        car.setReturnWhenEmptyLoadName(getReturnWhenEmptyLoadName());
093        car.setReturnWhenLoadedLoadName(getReturnWhenLoadedLoadName());
094        car.setNumber(getNumber());
095        car.setOwnerName(getOwnerName());
096        car.setRoadName(getRoadName());
097        car.setTypeName(getTypeName());
098        car.setCaboose(isCaboose());
099        car.setFred(hasFred());
100        car.setPassenger(isPassenger());
101        car.loaded = true;
102        return car;
103    }
104
105    public void setCarHazardous(boolean hazardous) {
106        boolean old = _hazardous;
107        _hazardous = hazardous;
108        if (!old == hazardous) {
109            setDirtyAndFirePropertyChange("car hazardous", old ? "true" : "false", hazardous ? "true" : "false"); // NOI18N
110        }
111    }
112
113    public boolean isCarHazardous() {
114        return _hazardous;
115    }
116
117    public boolean isCarLoadHazardous() {
118        return carLoads.isHazardous(getTypeName(), getLoadName());
119    }
120
121    /**
122     * Used to determine if the car is hazardous or the car's load is hazardous.
123     * 
124     * @return true if the car or car's load is hazardous.
125     */
126    public boolean isHazardous() {
127        return isCarHazardous() || isCarLoadHazardous();
128    }
129
130    public void setPassenger(boolean passenger) {
131        boolean old = _passenger;
132        _passenger = passenger;
133        if (!old == passenger) {
134            setDirtyAndFirePropertyChange("car passenger", old ? "true" : "false", passenger ? "true" : "false"); // NOI18N
135        }
136    }
137
138    public boolean isPassenger() {
139        return _passenger;
140    }
141
142    public void setFred(boolean fred) {
143        boolean old = _fred;
144        _fred = fred;
145        if (!old == fred) {
146            setDirtyAndFirePropertyChange("car has fred", old ? "true" : "false", fred ? "true" : "false"); // NOI18N
147        }
148    }
149
150    /**
151     * Used to determine if car has FRED (Flashing Rear End Device).
152     *
153     * @return true if car has FRED.
154     */
155    public boolean hasFred() {
156        return _fred;
157    }
158
159    public void setLoadName(String load) {
160        String old = _loadName;
161        _loadName = load;
162        if (!old.equals(load)) {
163            setDirtyAndFirePropertyChange(LOAD_CHANGED_PROPERTY, old, load);
164        }
165    }
166
167    /**
168     * The load name assigned to this car.
169     *
170     * @return The load name assigned to this car.
171     */
172    public String getLoadName() {
173        return _loadName;
174    }
175
176    public void setReturnWhenEmptyLoadName(String load) {
177        String old = _rweLoadName;
178        _rweLoadName = load;
179        if (!old.equals(load)) {
180            setDirtyAndFirePropertyChange(RWE_LOAD_CHANGED_PROPERTY, old, load);
181        }
182    }
183
184    public String getReturnWhenEmptyLoadName() {
185        return _rweLoadName;
186    }
187
188    public void setReturnWhenLoadedLoadName(String load) {
189        String old = _rwlLoadName;
190        _rwlLoadName = load;
191        if (!old.equals(load)) {
192            setDirtyAndFirePropertyChange(RWL_LOAD_CHANGED_PROPERTY, old, load);
193        }
194    }
195
196    public String getReturnWhenLoadedLoadName() {
197        return _rwlLoadName;
198    }
199
200    /**
201     * Gets the car's load's priority.
202     * 
203     * @return The car's load priority.
204     */
205    public String getLoadPriority() {
206        return (carLoads.getPriority(getTypeName(), getLoadName()));
207    }
208
209    /**
210     * Gets the car load's type, empty or load.
211     *
212     * @return type empty or type load
213     */
214    public String getLoadType() {
215        return (carLoads.getLoadType(getTypeName(), getLoadName()));
216    }
217
218    public String getPickupComment() {
219        return carLoads.getPickupComment(getTypeName(), getLoadName());
220    }
221
222    public String getDropComment() {
223        return carLoads.getDropComment(getTypeName(), getLoadName());
224    }
225
226    public void setLoadGeneratedFromStaging(boolean fromStaging) {
227        _loadGeneratedByStaging = fromStaging;
228    }
229
230    public boolean isLoadGeneratedFromStaging() {
231        return _loadGeneratedByStaging;
232    }
233
234    /**
235     * Used to keep track of which item in a schedule was used for this car.
236     * 
237     * @param id The ScheduleItem id for this car.
238     */
239    public void setScheduleItemId(String id) {
240        log.debug("Set schedule item id ({}) for car ({})", id, toString());
241        String old = _scheduleId;
242        _scheduleId = id;
243        if (!old.equals(id)) {
244            setDirtyAndFirePropertyChange(SCHEDULE_ID_CHANGED_PROPERTY, old, id);
245        }
246    }
247
248    public String getScheduleItemId() {
249        return _scheduleId;
250    }
251
252    public ScheduleItem getScheduleItem(Track track) {
253        ScheduleItem si = null;
254        // arrived at spur?
255        if (track != null && track.isSpur() && !getScheduleItemId().equals(NONE)) {
256            Schedule sch = track.getSchedule();
257            if (sch == null) {
258                log.error("Schedule null for car ({}) at spur ({})", toString(), track.getName());
259            } else {
260                si = sch.getItemById(getScheduleItemId());
261            }
262        }
263        return si;
264    }
265
266    /**
267     * Only here for backwards compatibility before version 5.1.4. The next load
268     * name for this car. Normally set by a schedule.
269     * 
270     * @param load the next load name.
271     */
272    public void setNextLoadName(String load) {
273        String old = _nextLoadName;
274        _nextLoadName = load;
275        if (!old.equals(load)) {
276            setDirtyAndFirePropertyChange(LOAD_CHANGED_PROPERTY, old, load);
277        }
278    }
279
280    public String getNextLoadName() {
281        return _nextLoadName;
282    }
283
284    @Override
285    public String getWeightTons() {
286        String weight = super.getWeightTons();
287        if (!_weightTons.equals(DEFAULT_WEIGHT)) {
288            return weight;
289        }
290        if (!isCaboose() && !isPassenger()) {
291            return weight;
292        }
293        // .9 tons/foot for caboose and passenger cars
294        try {
295            weight = Integer.toString((int) (Double.parseDouble(getLength()) * .9));
296        } catch (Exception e) {
297            log.debug("Car ({}) length not set for caboose or passenger car", toString());
298        }
299        return weight;
300    }
301
302    /**
303     * Returns a car's weight adjusted for load. An empty car's weight is 1/3
304     * the car's loaded weight.
305     */
306    @Override
307    public int getAdjustedWeightTons() {
308        int weightTons = 0;
309        try {
310            // get loaded weight
311            weightTons = Integer.parseInt(getWeightTons());
312            // adjust for empty weight if car is empty, 1/3 of loaded weight
313            if (!isCaboose() && !isPassenger() && getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
314                weightTons = weightTons / 3;
315            }
316        } catch (NumberFormatException e) {
317            log.debug("Car ({}) weight not set", toString());
318        }
319        return weightTons;
320    }
321
322    public void setWait(int count) {
323        int old = _wait;
324        _wait = count;
325        if (old != count) {
326            setDirtyAndFirePropertyChange(WAIT_CHANGED_PROPERTY, old, count);
327        }
328    }
329
330    public int getWait() {
331        return _wait;
332    }
333
334    /**
335     * Sets when this car will be picked up (day of the week)
336     *
337     * @param id See TrainSchedule.java
338     */
339    public void setPickupScheduleId(String id) {
340        String old = _pickupScheduleId;
341        _pickupScheduleId = id;
342        if (!old.equals(id)) {
343            setDirtyAndFirePropertyChange("car pickup schedule changes", old, id); // NOI18N
344        }
345    }
346
347    public String getPickupScheduleId() {
348        return _pickupScheduleId;
349    }
350
351    public String getPickupScheduleName() {
352        TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class)
353                .getScheduleById(getPickupScheduleId());
354        if (sch != null) {
355            return sch.getName();
356        }
357        return NONE;
358    }
359
360    /**
361     * Sets the final destination for a car.
362     *
363     * @param destination The final destination for this car.
364     */
365    public void setFinalDestination(Location destination) {
366        Location old = _finalDestination;
367        if (old != null) {
368            old.removePropertyChangeListener(this);
369        }
370        _finalDestination = destination;
371        if (_finalDestination != null) {
372            _finalDestination.addPropertyChangeListener(this);
373        }
374        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
375            setDirtyAndFirePropertyChange(FINAL_DESTINATION_CHANGED_PROPERTY, old, destination);
376        }
377    }
378
379    public Location getFinalDestination() {
380        return _finalDestination;
381    }
382    
383    public String getFinalDestinationName() {
384        if (_finalDestination != null) {
385            return _finalDestination.getName();
386        }
387        return NONE;
388    }
389    
390    public String getSplitFinalDestinationName() {
391        return TrainCommon.splitString(getFinalDestinationName());
392    }
393
394    public void setFinalDestinationTrack(Track track) {
395        Track old = _finalDestTrack;
396        _finalDestTrack = track;
397        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
398            if (old != null) {
399                old.removePropertyChangeListener(this);
400                old.deleteReservedInRoute(this);
401            }
402            if (_finalDestTrack != null) {
403                _finalDestTrack.addReservedInRoute(this);
404                _finalDestTrack.addPropertyChangeListener(this);
405            }
406            setDirtyAndFirePropertyChange(FINAL_DESTINATION_TRACK_CHANGED_PROPERTY, old, track);
407        }
408    }
409
410    public Track getFinalDestinationTrack() {
411        return _finalDestTrack;
412    }
413
414    public String getFinalDestinationTrackName() {
415        if (_finalDestTrack != null) {
416            return _finalDestTrack.getName();
417        }
418        return NONE;
419    }
420    
421    public String getSplitFinalDestinationTrackName() {
422        return TrainCommon.splitString(getFinalDestinationTrackName());
423    }
424
425    public void setPreviousFinalDestination(Location location) {
426        _previousFinalDestination = location;
427    }
428
429    public Location getPreviousFinalDestination() {
430        return _previousFinalDestination;
431    }
432
433    public void setPreviousFinalDestinationTrack(Track track) {
434        _previousFinalDestTrack = track;
435    }
436
437    public Track getPreviousFinalDestinationTrack() {
438        return _previousFinalDestTrack;
439    }
440
441    public void setPreviousScheduleId(String id) {
442        _previousScheduleId = id;
443    }
444
445    public String getPreviousScheduleId() {
446        return _previousScheduleId;
447    }
448
449    public void setReturnWhenEmptyDestination(Location destination) {
450        Location old = _rweDestination;
451        _rweDestination = destination;
452        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
453            setDirtyAndFirePropertyChange(RETURN_WHEN_EMPTY_CHANGED_PROPERTY, null, null);
454        }
455    }
456
457    public Location getReturnWhenEmptyDestination() {
458        return _rweDestination;
459    }
460
461    public String getReturnWhenEmptyDestinationName() {
462        if (getReturnWhenEmptyDestination() != null) {
463            return getReturnWhenEmptyDestination().getName();
464        }
465        return NONE;
466    }
467    
468    public String getSplitReturnWhenEmptyDestinationName() {
469        return TrainCommon.splitString(getReturnWhenEmptyDestinationName());
470    }
471    
472    public void setReturnWhenEmptyDestTrack(Track track) {
473        Track old = _rweDestTrack;
474        _rweDestTrack = track;
475        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
476            setDirtyAndFirePropertyChange(RETURN_WHEN_EMPTY_CHANGED_PROPERTY, null, null);
477        }
478    }
479
480    public Track getReturnWhenEmptyDestTrack() {
481        return _rweDestTrack;
482    }
483
484    public String getReturnWhenEmptyDestTrackName() {
485        if (getReturnWhenEmptyDestTrack() != null) {
486            return getReturnWhenEmptyDestTrack().getName();
487        }
488        return NONE;
489    }
490    
491    public String getSplitReturnWhenEmptyDestinationTrackName() {
492        return TrainCommon.splitString(getReturnWhenEmptyDestTrackName());
493    }
494
495    public void setReturnWhenLoadedDestination(Location destination) {
496        Location old = _rwlDestination;
497        _rwlDestination = destination;
498        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
499            setDirtyAndFirePropertyChange(RETURN_WHEN_LOADED_CHANGED_PROPERTY, null, null);
500        }
501    }
502
503    public Location getReturnWhenLoadedDestination() {
504        return _rwlDestination;
505    }
506
507    public String getReturnWhenLoadedDestinationName() {
508        if (getReturnWhenLoadedDestination() != null) {
509            return getReturnWhenLoadedDestination().getName();
510        }
511        return NONE;
512    }
513
514    public void setReturnWhenLoadedDestTrack(Track track) {
515        Track old = _rwlDestTrack;
516        _rwlDestTrack = track;
517        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
518            setDirtyAndFirePropertyChange(RETURN_WHEN_LOADED_CHANGED_PROPERTY, null, null);
519        }
520    }
521
522    public Track getReturnWhenLoadedDestTrack() {
523        return _rwlDestTrack;
524    }
525
526    public String getReturnWhenLoadedDestTrackName() {
527        if (getReturnWhenLoadedDestTrack() != null) {
528            return getReturnWhenLoadedDestTrack().getName();
529        }
530        return NONE;
531    }
532
533    /**
534     * Used to determine is car has been given a Return When Loaded (RWL)
535     * address or custom load
536     * 
537     * @return true if car has RWL
538     */
539    protected boolean isRwlEnabled() {
540        if (!getReturnWhenLoadedLoadName().equals(carLoads.getDefaultLoadName()) ||
541                getReturnWhenLoadedDestination() != null) {
542            return true;
543        }
544        return false;
545    }
546
547    public void setCaboose(boolean caboose) {
548        boolean old = _caboose;
549        _caboose = caboose;
550        if (!old == caboose) {
551            setDirtyAndFirePropertyChange("car is caboose", old ? "true" : "false", caboose ? "true" : "false"); // NOI18N
552        }
553    }
554
555    public boolean isCaboose() {
556        return _caboose;
557    }
558
559    public void setUtility(boolean utility) {
560        boolean old = _utility;
561        _utility = utility;
562        if (!old == utility) {
563            setDirtyAndFirePropertyChange("car is utility", old ? "true" : "false", utility ? "true" : "false"); // NOI18N
564        }
565    }
566
567    public boolean isUtility() {
568        return _utility;
569    }
570
571    /**
572     * Used to determine if car is performing a local move. A local move is when
573     * a car is moved to a different track at the same location. Car has to be
574     * assigned to a train.
575     * 
576     * @return true if local move
577     */
578    public boolean isLocalMove() {
579        if (getTrain() == null && getLocation() != null) {
580            return getSplitLocationName().equals(getSplitDestinationName());
581        }
582        if (getRouteLocation() == null || getRouteDestination() == null) {
583            return false;
584        }
585        if (getRouteLocation().equals(getRouteDestination()) && getTrack() != null) {
586            return true;
587        }
588        if (getTrain().isLocalSwitcher() &&
589                getRouteLocation().getSplitName()
590                        .equals(getRouteDestination().getSplitName()) &&
591                getTrack() != null) {
592            return true;
593        }
594        // look for sequential locations with the "same" name
595        if (getRouteLocation().getSplitName().equals(
596                getRouteDestination().getSplitName()) && getTrain().getRoute() != null) {
597            boolean foundRl = false;
598            for (RouteLocation rl : getTrain().getRoute().getLocationsBySequenceList()) {
599                if (foundRl) {
600                    if (getRouteDestination().getSplitName()
601                            .equals(rl.getSplitName())) {
602                        // user can specify the "same" location two more more
603                        // times in a row
604                        if (getRouteDestination() != rl) {
605                            continue;
606                        } else {
607                            return true;
608                        }
609                    } else {
610                        return false;
611                    }
612                }
613                if (getRouteLocation().equals(rl)) {
614                    foundRl = true;
615                }
616            }
617        }
618        return false;
619    }
620
621    /**
622     * A kernel is a group of cars that are switched as a unit.
623     * 
624     * @param kernel The assigned Kernel for this car.
625     */
626    public void setKernel(Kernel kernel) {
627        if (_kernel == kernel) {
628            return;
629        }
630        String old = "";
631        if (_kernel != null) {
632            old = _kernel.getName();
633            _kernel.delete(this);
634        }
635        _kernel = kernel;
636        String newName = "";
637        if (_kernel != null) {
638            _kernel.add(this);
639            newName = _kernel.getName();
640        }
641        if (!old.equals(newName)) {
642            setDirtyAndFirePropertyChange(KERNEL_NAME_CHANGED_PROPERTY, old, newName); // NOI18N
643        }
644    }
645
646    public Kernel getKernel() {
647        return _kernel;
648    }
649
650    public String getKernelName() {
651        if (_kernel != null) {
652            return _kernel.getName();
653        }
654        return NONE;
655    }
656
657    /**
658     * Used to determine if car is lead car in a kernel
659     * 
660     * @return true if lead car in a kernel
661     */
662    public boolean isLead() {
663        if (getKernel() != null) {
664            return getKernel().isLead(this);
665        }
666        return false;
667    }
668
669    /**
670     * Updates all cars in a kernel. After the update, the cars will all have
671     * the same final destination, load, and next load.
672     */
673    public void updateKernel() {
674        if (isLead()) {
675            for (Car car : getKernel().getCars()) {
676                car.setScheduleItemId(getScheduleItemId());
677                car.setFinalDestination(getFinalDestination());
678                car.setFinalDestinationTrack(getFinalDestinationTrack());
679                car.setLoadGeneratedFromStaging(isLoadGeneratedFromStaging());
680                if (InstanceManager.getDefault(CarLoads.class).containsName(car.getTypeName(), getLoadName())) {
681                    car.setLoadName(getLoadName());
682                }
683            }
684        }
685    }
686
687    /**
688     * Used to determine if a car can be set out at a destination (location).
689     * Track is optional. In addition to all of the tests that checkDestination
690     * performs, spurs with schedules are also checked.
691     *
692     * @return status OKAY, TYPE, ROAD, LENGTH, ERROR_TRACK, CAPACITY, SCHEDULE,
693     *         CUSTOM
694     */
695    @Override
696    public String checkDestination(Location destination, Track track) {
697        String status = super.checkDestination(destination, track);
698        if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
699            return status;
700        }
701        // now check to see if the track has a schedule
702        if (track == null) {
703            return status;
704        }
705        String statusSchedule = track.checkSchedule(this);
706        if (status.startsWith(Track.LENGTH) && statusSchedule.equals(Track.OKAY)) {
707            return status;
708        }
709        return statusSchedule;
710    }
711
712    /**
713     * Sets the car's destination on the layout
714     *
715     * @param track (yard, spur, staging, or interchange track)
716     * @return "okay" if successful, "type" if the rolling stock's type isn't
717     *         acceptable, or "length" if the rolling stock length didn't fit,
718     *         or Schedule if the destination will not accept the car because
719     *         the spur has a schedule and the car doesn't meet the schedule
720     *         requirements. Also changes the car load status when the car
721     *         reaches its destination.
722     */
723    @Override
724    public String setDestination(Location destination, Track track) {
725        return setDestination(destination, track, false);
726    }
727
728    /**
729     * Sets the car's destination on the layout
730     *
731     * @param track (yard, spur, staging, or interchange track)
732     * @param force when true ignore track length, type, and road when setting
733     *              destination
734     * @return "okay" if successful, "type" if the rolling stock's type isn't
735     *         acceptable, or "length" if the rolling stock length didn't fit,
736     *         or Schedule if the destination will not accept the car because
737     *         the spur has a schedule and the car doesn't meet the schedule
738     *         requirements. Also changes the car load status when the car
739     *         reaches its destination.
740     */
741    @Override
742    public String setDestination(Location destination, Track track, boolean force) {
743        // save destination name and track in case car has reached its
744        // destination
745        String destinationName = getDestinationName();
746        Track destinationTrack = getDestinationTrack();
747        String status = super.setDestination(destination, track, force);
748        // return if not Okay
749        if (!status.equals(Track.OKAY)) {
750            return status;
751        }
752        // now check to see if the track has a schedule
753        if (track != null && destinationTrack != track && loaded) {
754            status = track.scheduleNext(this);
755            if (!status.equals(Track.OKAY)) {
756                return status;
757            }
758        }
759        // done?
760        if (destinationName.equals(NONE) || (destination != null) || getTrain() == null) {
761            return status;
762        }
763        // car was in a train and has been dropped off, update load, RWE could
764        // set a new final destination
765        loadNext(destinationTrack);
766        return status;
767    }
768
769    /**
770     * Called when setting a car's destination to this spur. Loads the car with
771     * a final destination which is the ship address for the schedule item.
772     * 
773     * @param scheduleItem The schedule item to be applied this this car
774     */
775    public void loadNext(ScheduleItem scheduleItem) {
776        if (scheduleItem == null) {
777            return; // should never be null
778        }
779        // set the car's final destination and track
780        setFinalDestination(scheduleItem.getDestination());
781        setFinalDestinationTrack(scheduleItem.getDestinationTrack());
782        // bump hit count for this schedule item
783        scheduleItem.setHits(scheduleItem.getHits() + 1);
784        // set all cars in kernel same final destination
785        updateKernel();
786    }
787
788    /**
789     * Called when car is delivered to track. Updates the car's wait, pickup
790     * day, and load if spur. If staging, can swap default loads, force load to
791     * default empty, or replace custom loads with the default empty load. Can
792     * trigger RWE or RWL.
793     * 
794     * @param track the destination track for this car
795     */
796    public void loadNext(Track track) {
797        setLoadGeneratedFromStaging(false);
798        if (track != null) {
799            if (track.isSpur()) {
800                ScheduleItem si = getScheduleItem(track);
801                if (si == null) {
802                    log.debug("Schedule item ({}) is null for car ({}) at spur ({})", getScheduleItemId(), toString(),
803                            track.getName());
804                } else {
805                    setWait(si.getWait());
806                    setPickupScheduleId(si.getPickupTrainScheduleId());
807                }
808                updateLoad(track);
809            }
810            // update load optionally when car reaches staging
811            else if (track.isStaging()) {
812                if (track.isLoadSwapEnabled() && getLoadName().equals(carLoads.getDefaultEmptyName())) {
813                    setLoadLoaded();
814                } else if ((track.isLoadSwapEnabled() || track.isLoadEmptyEnabled()) &&
815                        getLoadName().equals(carLoads.getDefaultLoadName())) {
816                    setLoadEmpty();
817                } else if (track.isRemoveCustomLoadsEnabled() &&
818                        !getLoadName().equals(carLoads.getDefaultEmptyName()) &&
819                        !getLoadName().equals(carLoads.getDefaultLoadName())) {
820                    // remove this car's final destination if it has one
821                    setFinalDestination(null);
822                    setFinalDestinationTrack(null);
823                    if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY) && isRwlEnabled()) {
824                        setLoadLoaded();
825                        // car arriving into staging with the RWE load?
826                    } else if (getLoadName().equals(getReturnWhenEmptyLoadName())) {
827                        setLoadName(carLoads.getDefaultEmptyName());
828                    } else {
829                        setLoadEmpty(); // note that RWE sets the car's final
830                                        // destination
831                    }
832                }
833            }
834        }
835    }
836
837    /**
838     * Updates a car's load when placed at a spur. Load change delayed if wait
839     * count is greater than zero. 
840     * 
841     * @param track The spur the car is sitting on
842     */
843    public void updateLoad(Track track) {
844        if (track.isDisableLoadChangeEnabled()) {
845            return;
846        }
847        if (getWait() > 0) {
848            return; // change load name when wait count reaches 0
849        }
850        // arriving at spur with a schedule?
851        String loadName = NONE;
852        ScheduleItem si = getScheduleItem(track);
853        if (si != null) {
854            loadName = si.getShipLoadName(); // can be NONE
855        } else {
856            // for backwards compatibility before version 5.1.4
857            log.debug("Schedule item ({}) is null for car ({}) at spur ({}), using next load name", getScheduleItemId(),
858                    toString(), track.getName());
859            loadName = getNextLoadName();
860        }
861        setNextLoadName(NONE);
862        if (!loadName.equals(NONE)) {
863            setLoadName(loadName);
864            // RWE or RWL load and no destination?
865            if (getLoadName().equals(getReturnWhenEmptyLoadName()) && getFinalDestination() == null) {
866                setReturnWhenEmpty();
867            } else if (getLoadName().equals(getReturnWhenLoadedLoadName()) && getFinalDestination() == null) {
868                setReturnWhenLoaded();
869            }
870        } else {
871            // flip load names
872            if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
873                setLoadLoaded();
874            } else {
875                setLoadEmpty();
876            }
877        }
878        setScheduleItemId(Car.NONE);
879    }
880
881    /**
882     * Sets the car's load to empty, triggers RWE load and destination if
883     * enabled.
884     */
885    private void setLoadEmpty() {
886        if (!getLoadName().equals(getReturnWhenEmptyLoadName())) {
887            setLoadName(getReturnWhenEmptyLoadName()); // default RWE load is
888                                                       // the "E" load
889            setReturnWhenEmpty();
890        }
891    }
892
893    /*
894     * Don't set return address if in staging with the same RWE address and
895     * don't set return address if at the RWE address
896     */
897    private void setReturnWhenEmpty() {
898        if (getReturnWhenEmptyDestination() != null &&
899                (getLocation() != getReturnWhenEmptyDestination() ||
900                        (!getReturnWhenEmptyDestination().isStaging() &&
901                                getTrack() != getReturnWhenEmptyDestTrack()))) {
902            setFinalDestination(getReturnWhenEmptyDestination());
903            if (getReturnWhenEmptyDestTrack() != null) {
904                setFinalDestinationTrack(getReturnWhenEmptyDestTrack());
905            }
906            log.debug("Car ({}) has return when empty destination ({}, {}) load {}", toString(),
907                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
908        }
909    }
910
911    /**
912     * Sets the car's load to loaded, triggers RWL load and destination if
913     * enabled.
914     */
915    private void setLoadLoaded() {
916        if (!getLoadName().equals(getReturnWhenLoadedLoadName())) {
917            setLoadName(getReturnWhenLoadedLoadName()); // default RWL load is
918                                                        // the "L" load
919            setReturnWhenLoaded();
920        }
921    }
922
923    /*
924     * Don't set return address if in staging with the same RWL address and
925     * don't set return address if at the RWL address
926     */
927    private void setReturnWhenLoaded() {
928        if (getReturnWhenLoadedDestination() != null &&
929                (getLocation() != getReturnWhenLoadedDestination() ||
930                        (!getReturnWhenLoadedDestination().isStaging() &&
931                                getTrack() != getReturnWhenLoadedDestTrack()))) {
932            setFinalDestination(getReturnWhenLoadedDestination());
933            if (getReturnWhenLoadedDestTrack() != null) {
934                setFinalDestinationTrack(getReturnWhenLoadedDestTrack());
935            }
936            log.debug("Car ({}) has return when loaded destination ({}, {}) load {}", toString(),
937                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
938        }
939    }
940
941    public String getTypeExtensions() {
942        StringBuffer buf = new StringBuffer();
943        if (isCaboose()) {
944            buf.append(EXTENSION_REGEX + CABOOSE_EXTENSION);
945        }
946        if (hasFred()) {
947            buf.append(EXTENSION_REGEX + FRED_EXTENSION);
948        }
949        if (isPassenger()) {
950            buf.append(EXTENSION_REGEX + PASSENGER_EXTENSION + EXTENSION_REGEX + getBlocking());
951        }
952        if (isUtility()) {
953            buf.append(EXTENSION_REGEX + UTILITY_EXTENSION);
954        }
955        if (isCarHazardous()) {
956            buf.append(EXTENSION_REGEX + HAZARDOUS_EXTENSION);
957        }
958        return buf.toString();
959    }
960
961    @Override
962    public void reset() {
963        setScheduleItemId(getPreviousScheduleId()); // revert to previous
964        setNextLoadName(NONE);
965        setFinalDestination(getPreviousFinalDestination());
966        setFinalDestinationTrack(getPreviousFinalDestinationTrack());
967        if (isLoadGeneratedFromStaging()) {
968            setLoadGeneratedFromStaging(false);
969            setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
970        }
971        super.reset();
972    }
973
974    @Override
975    public void dispose() {
976        setKernel(null);
977        setFinalDestination(null); // removes property change listener
978        setFinalDestinationTrack(null); // removes property change listener
979        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
980        InstanceManager.getDefault(CarLengths.class).removePropertyChangeListener(this);
981        super.dispose();
982    }
983
984    // used to stop a track's schedule from bumping when loading car database
985    private boolean loaded = false;
986
987    /**
988     * Construct this Entry from XML. This member has to remain synchronized
989     * with the detailed DTD in operations-cars.dtd
990     *
991     * @param e Car XML element
992     */
993    public Car(org.jdom2.Element e) {
994        super(e);
995        loaded = true;
996        org.jdom2.Attribute a;
997        if ((a = e.getAttribute(Xml.PASSENGER)) != null) {
998            _passenger = a.getValue().equals(Xml.TRUE);
999        }
1000        if ((a = e.getAttribute(Xml.HAZARDOUS)) != null) {
1001            _hazardous = a.getValue().equals(Xml.TRUE);
1002        }
1003        if ((a = e.getAttribute(Xml.CABOOSE)) != null) {
1004            _caboose = a.getValue().equals(Xml.TRUE);
1005        }
1006        if ((a = e.getAttribute(Xml.FRED)) != null) {
1007            _fred = a.getValue().equals(Xml.TRUE);
1008        }
1009        if ((a = e.getAttribute(Xml.UTILITY)) != null) {
1010            _utility = a.getValue().equals(Xml.TRUE);
1011        }
1012        if ((a = e.getAttribute(Xml.KERNEL)) != null) {
1013            Kernel k = InstanceManager.getDefault(KernelManager.class).getKernelByName(a.getValue());
1014            if (k != null) {
1015                setKernel(k);
1016                if ((a = e.getAttribute(Xml.LEAD_KERNEL)) != null && a.getValue().equals(Xml.TRUE)) {
1017                    _kernel.setLead(this);
1018                }
1019            } else {
1020                log.error("Kernel {} does not exist", a.getValue());
1021            }
1022        }
1023        if ((a = e.getAttribute(Xml.LOAD)) != null) {
1024            _loadName = a.getValue();
1025        }
1026        if ((a = e.getAttribute(Xml.LOAD_FROM_STAGING)) != null && a.getValue().equals(Xml.TRUE)) {
1027            setLoadGeneratedFromStaging(true);
1028        }
1029
1030        if ((a = e.getAttribute(Xml.WAIT)) != null) {
1031            try {
1032                _wait = Integer.parseInt(a.getValue());
1033            } catch (NumberFormatException nfe) {
1034                log.error("Wait count ({}) for car ({}) isn't a valid number!", a.getValue(), toString());
1035            }
1036        }
1037        if ((a = e.getAttribute(Xml.PICKUP_SCHEDULE_ID)) != null) {
1038            _pickupScheduleId = a.getValue();
1039        }
1040        if ((a = e.getAttribute(Xml.SCHEDULE_ID)) != null) {
1041            _scheduleId = a.getValue();
1042        }
1043        // for backwards compatibility before version 5.1.4
1044        if ((a = e.getAttribute(Xml.NEXT_LOAD)) != null) {
1045            _nextLoadName = a.getValue();
1046        }
1047        if ((a = e.getAttribute(Xml.NEXT_DEST_ID)) != null) {
1048            setFinalDestination(InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1049        }
1050        if (getFinalDestination() != null && (a = e.getAttribute(Xml.NEXT_DEST_TRACK_ID)) != null) {
1051            setFinalDestinationTrack(getFinalDestination().getTrackById(a.getValue()));
1052        }
1053        if ((a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_ID)) != null) {
1054            setPreviousFinalDestination(
1055                    InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1056        }
1057        if (getPreviousFinalDestination() != null && (a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID)) != null) {
1058            setPreviousFinalDestinationTrack(getPreviousFinalDestination().getTrackById(a.getValue()));
1059        }
1060        if ((a = e.getAttribute(Xml.PREVIOUS_SCHEDULE_ID)) != null) {
1061            setPreviousScheduleId(a.getValue());
1062        }
1063        if ((a = e.getAttribute(Xml.RWE_DEST_ID)) != null) {
1064            _rweDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1065        }
1066        if (_rweDestination != null && (a = e.getAttribute(Xml.RWE_DEST_TRACK_ID)) != null) {
1067            _rweDestTrack = _rweDestination.getTrackById(a.getValue());
1068        }
1069        if ((a = e.getAttribute(Xml.RWE_LOAD)) != null) {
1070            _rweLoadName = a.getValue();
1071        }
1072        if ((a = e.getAttribute(Xml.RWL_DEST_ID)) != null) {
1073            _rwlDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1074        }
1075        if (_rwlDestination != null && (a = e.getAttribute(Xml.RWL_DEST_TRACK_ID)) != null) {
1076            _rwlDestTrack = _rwlDestination.getTrackById(a.getValue());
1077        }
1078        if ((a = e.getAttribute(Xml.RWL_LOAD)) != null) {
1079            _rwlLoadName = a.getValue();
1080        }
1081        addPropertyChangeListeners();
1082    }
1083
1084    /**
1085     * Create an XML element to represent this Entry. This member has to remain
1086     * synchronized with the detailed DTD in operations-cars.dtd.
1087     *
1088     * @return Contents in a JDOM Element
1089     */
1090    public org.jdom2.Element store() {
1091        org.jdom2.Element e = new org.jdom2.Element(Xml.CAR);
1092        super.store(e);
1093        if (isPassenger()) {
1094            e.setAttribute(Xml.PASSENGER, isPassenger() ? Xml.TRUE : Xml.FALSE);
1095        }
1096        if (isCarHazardous()) {
1097            e.setAttribute(Xml.HAZARDOUS, isCarHazardous() ? Xml.TRUE : Xml.FALSE);
1098        }
1099        if (isCaboose()) {
1100            e.setAttribute(Xml.CABOOSE, isCaboose() ? Xml.TRUE : Xml.FALSE);
1101        }
1102        if (hasFred()) {
1103            e.setAttribute(Xml.FRED, hasFred() ? Xml.TRUE : Xml.FALSE);
1104        }
1105        if (isUtility()) {
1106            e.setAttribute(Xml.UTILITY, isUtility() ? Xml.TRUE : Xml.FALSE);
1107        }
1108        if (getKernel() != null) {
1109            e.setAttribute(Xml.KERNEL, getKernelName());
1110            if (isLead()) {
1111                e.setAttribute(Xml.LEAD_KERNEL, Xml.TRUE);
1112            }
1113        }
1114
1115        e.setAttribute(Xml.LOAD, getLoadName());
1116
1117        if (isLoadGeneratedFromStaging()) {
1118            e.setAttribute(Xml.LOAD_FROM_STAGING, Xml.TRUE);
1119        }
1120
1121        if (getWait() != 0) {
1122            e.setAttribute(Xml.WAIT, Integer.toString(getWait()));
1123        }
1124
1125        if (!getPickupScheduleId().equals(NONE)) {
1126            e.setAttribute(Xml.PICKUP_SCHEDULE_ID, getPickupScheduleId());
1127        }
1128
1129        if (!getScheduleItemId().equals(NONE)) {
1130            e.setAttribute(Xml.SCHEDULE_ID, getScheduleItemId());
1131        }
1132
1133        // for backwards compatibility before version 5.1.4
1134        if (!getNextLoadName().equals(NONE)) {
1135            e.setAttribute(Xml.NEXT_LOAD, getNextLoadName());
1136        }
1137
1138        if (getFinalDestination() != null) {
1139            e.setAttribute(Xml.NEXT_DEST_ID, getFinalDestination().getId());
1140            if (getFinalDestinationTrack() != null) {
1141                e.setAttribute(Xml.NEXT_DEST_TRACK_ID, getFinalDestinationTrack().getId());
1142            }
1143        }
1144
1145        if (getPreviousFinalDestination() != null) {
1146            e.setAttribute(Xml.PREVIOUS_NEXT_DEST_ID, getPreviousFinalDestination().getId());
1147            if (getPreviousFinalDestinationTrack() != null) {
1148                e.setAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID, getPreviousFinalDestinationTrack().getId());
1149            }
1150        }
1151
1152        if (!getPreviousScheduleId().equals(NONE)) {
1153            e.setAttribute(Xml.PREVIOUS_SCHEDULE_ID, getPreviousScheduleId());
1154        }
1155
1156        if (getReturnWhenEmptyDestination() != null) {
1157            e.setAttribute(Xml.RWE_DEST_ID, getReturnWhenEmptyDestination().getId());
1158            if (getReturnWhenEmptyDestTrack() != null) {
1159                e.setAttribute(Xml.RWE_DEST_TRACK_ID, getReturnWhenEmptyDestTrack().getId());
1160            }
1161        }
1162        if (!getReturnWhenEmptyLoadName().equals(carLoads.getDefaultEmptyName())) {
1163            e.setAttribute(Xml.RWE_LOAD, getReturnWhenEmptyLoadName());
1164        }
1165
1166        if (getReturnWhenLoadedDestination() != null) {
1167            e.setAttribute(Xml.RWL_DEST_ID, getReturnWhenLoadedDestination().getId());
1168            if (getReturnWhenLoadedDestTrack() != null) {
1169                e.setAttribute(Xml.RWL_DEST_TRACK_ID, getReturnWhenLoadedDestTrack().getId());
1170            }
1171        }
1172        if (!getReturnWhenLoadedLoadName().equals(carLoads.getDefaultLoadName())) {
1173            e.setAttribute(Xml.RWL_LOAD, getReturnWhenLoadedLoadName());
1174        }
1175
1176        return e;
1177    }
1178
1179    @Override
1180    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1181        // Set dirty
1182        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
1183        super.setDirtyAndFirePropertyChange(p, old, n);
1184    }
1185
1186    private void addPropertyChangeListeners() {
1187        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
1188        InstanceManager.getDefault(CarLengths.class).addPropertyChangeListener(this);
1189    }
1190
1191    @Override
1192    public void propertyChange(PropertyChangeEvent e) {
1193        super.propertyChange(e);
1194        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY)) {
1195            if (e.getOldValue().equals(getTypeName())) {
1196                log.debug("Car ({}) sees type name change old: ({}) new: ({})", toString(), e.getOldValue(),
1197                        e.getNewValue()); // NOI18N
1198                setTypeName((String) e.getNewValue());
1199            }
1200        }
1201        if (e.getPropertyName().equals(CarLengths.CARLENGTHS_NAME_CHANGED_PROPERTY)) {
1202            if (e.getOldValue().equals(getLength())) {
1203                log.debug("Car ({}) sees length name change old: ({}) new: ({})", toString(), e.getOldValue(),
1204                        e.getNewValue()); // NOI18N
1205                setLength((String) e.getNewValue());
1206            }
1207        }
1208        if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) {
1209            if (e.getSource() == _finalDestination) {
1210                log.debug("delete final destination for car: ({})", toString());
1211                setFinalDestination(null);
1212            }
1213        }
1214        if (e.getPropertyName().equals(Track.DISPOSE_CHANGED_PROPERTY)) {
1215            if (e.getSource() == _finalDestTrack) {
1216                log.debug("delete final destination for car: ({})", toString());
1217                setFinalDestinationTrack(null);
1218            }
1219        }
1220    }
1221
1222    private final static Logger log = LoggerFactory.getLogger(Car.class);
1223
1224}