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