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        if (getTrain() != null) {
355            return getPickupTime();
356        }
357        TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class)
358                .getScheduleById(getPickupScheduleId());
359        if (sch != null) {
360            return sch.getName();
361        }
362        return NONE;
363    }
364
365    /**
366     * Sets the final destination for a car.
367     *
368     * @param destination The final destination for this car.
369     */
370    public void setFinalDestination(Location destination) {
371        Location old = _finalDestination;
372        if (old != null) {
373            old.removePropertyChangeListener(this);
374        }
375        _finalDestination = destination;
376        if (_finalDestination != null) {
377            _finalDestination.addPropertyChangeListener(this);
378        }
379        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
380            setRoutePath(NONE);
381            setDirtyAndFirePropertyChange(FINAL_DESTINATION_CHANGED_PROPERTY, old, destination);
382        }
383    }
384
385    public Location getFinalDestination() {
386        return _finalDestination;
387    }
388    
389    public String getFinalDestinationName() {
390        if (getFinalDestination() != null) {
391            return getFinalDestination().getName();
392        }
393        return NONE;
394    }
395    
396    public String getSplitFinalDestinationName() {
397        return TrainCommon.splitString(getFinalDestinationName());
398    }
399
400    public void setFinalDestinationTrack(Track track) {
401        Track old = _finalDestTrack;
402        _finalDestTrack = track;
403        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
404            if (old != null) {
405                old.removePropertyChangeListener(this);
406                old.deleteReservedInRoute(this);
407            }
408            if (_finalDestTrack != null) {
409                _finalDestTrack.addReservedInRoute(this);
410                _finalDestTrack.addPropertyChangeListener(this);
411            }
412            setDirtyAndFirePropertyChange(FINAL_DESTINATION_TRACK_CHANGED_PROPERTY, old, track);
413        }
414    }
415
416    public Track getFinalDestinationTrack() {
417        return _finalDestTrack;
418    }
419
420    public String getFinalDestinationTrackName() {
421        if (getFinalDestinationTrack() != null) {
422            return getFinalDestinationTrack().getName();
423        }
424        return NONE;
425    }
426    
427    public String getSplitFinalDestinationTrackName() {
428        return TrainCommon.splitString(getFinalDestinationTrackName());
429    }
430
431    public void setPreviousFinalDestination(Location location) {
432        _previousFinalDestination = location;
433    }
434
435    public Location getPreviousFinalDestination() {
436        return _previousFinalDestination;
437    }
438
439    public String getPreviousFinalDestinationName() {
440        if (getPreviousFinalDestination() != null) {
441            return getPreviousFinalDestination().getName();
442        }
443        return NONE;
444    }
445
446    public void setPreviousFinalDestinationTrack(Track track) {
447        _previousFinalDestTrack = track;
448    }
449
450    public Track getPreviousFinalDestinationTrack() {
451        return _previousFinalDestTrack;
452    }
453
454    public String getPreviousFinalDestinationTrackName() {
455        if (getPreviousFinalDestinationTrack() != null) {
456            return getPreviousFinalDestinationTrack().getName();
457        }
458        return NONE;
459    }
460
461    public void setPreviousScheduleId(String id) {
462        _previousScheduleId = id;
463    }
464
465    public String getPreviousScheduleId() {
466        return _previousScheduleId;
467    }
468
469    public void setReturnWhenEmptyDestination(Location destination) {
470        Location old = _rweDestination;
471        _rweDestination = destination;
472        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
473            setDirtyAndFirePropertyChange(RETURN_WHEN_EMPTY_CHANGED_PROPERTY, null, null);
474        }
475    }
476
477    public Location getReturnWhenEmptyDestination() {
478        return _rweDestination;
479    }
480
481    public String getReturnWhenEmptyDestinationName() {
482        if (getReturnWhenEmptyDestination() != null) {
483            return getReturnWhenEmptyDestination().getName();
484        }
485        return NONE;
486    }
487    
488    public String getSplitReturnWhenEmptyDestinationName() {
489        return TrainCommon.splitString(getReturnWhenEmptyDestinationName());
490    }
491    
492    public void setReturnWhenEmptyDestTrack(Track track) {
493        Track old = _rweDestTrack;
494        _rweDestTrack = track;
495        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
496            setDirtyAndFirePropertyChange(RETURN_WHEN_EMPTY_CHANGED_PROPERTY, null, null);
497        }
498    }
499
500    public Track getReturnWhenEmptyDestTrack() {
501        return _rweDestTrack;
502    }
503
504    public String getReturnWhenEmptyDestTrackName() {
505        if (getReturnWhenEmptyDestTrack() != null) {
506            return getReturnWhenEmptyDestTrack().getName();
507        }
508        return NONE;
509    }
510    
511    public String getSplitReturnWhenEmptyDestinationTrackName() {
512        return TrainCommon.splitString(getReturnWhenEmptyDestTrackName());
513    }
514
515    public void setReturnWhenLoadedDestination(Location destination) {
516        Location old = _rwlDestination;
517        _rwlDestination = destination;
518        if ((old != null && !old.equals(destination)) || (destination != null && !destination.equals(old))) {
519            setDirtyAndFirePropertyChange(RETURN_WHEN_LOADED_CHANGED_PROPERTY, null, null);
520        }
521    }
522
523    public Location getReturnWhenLoadedDestination() {
524        return _rwlDestination;
525    }
526
527    public String getReturnWhenLoadedDestinationName() {
528        if (getReturnWhenLoadedDestination() != null) {
529            return getReturnWhenLoadedDestination().getName();
530        }
531        return NONE;
532    }
533
534    public void setReturnWhenLoadedDestTrack(Track track) {
535        Track old = _rwlDestTrack;
536        _rwlDestTrack = track;
537        if ((old != null && !old.equals(track)) || (track != null && !track.equals(old))) {
538            setDirtyAndFirePropertyChange(RETURN_WHEN_LOADED_CHANGED_PROPERTY, null, null);
539        }
540    }
541
542    public Track getReturnWhenLoadedDestTrack() {
543        return _rwlDestTrack;
544    }
545
546    public String getReturnWhenLoadedDestTrackName() {
547        if (getReturnWhenLoadedDestTrack() != null) {
548            return getReturnWhenLoadedDestTrack().getName();
549        }
550        return NONE;
551    }
552
553    /**
554     * Used to determine is car has been given a Return When Loaded (RWL)
555     * address or custom load
556     * 
557     * @return true if car has RWL
558     */
559    protected boolean isRwlEnabled() {
560        if (!getReturnWhenLoadedLoadName().equals(carLoads.getDefaultLoadName()) ||
561                getReturnWhenLoadedDestination() != null) {
562            return true;
563        }
564        return false;
565    }
566
567    public void setRoutePath(String routePath) {
568        String old = _routePath;
569        _routePath = routePath;
570        if (!old.equals(routePath)) {
571            setDirtyAndFirePropertyChange("Route path change", old, routePath);
572        }
573    }
574
575    public String getRoutePath() {
576        return _routePath;
577    }
578
579    public void setCaboose(boolean caboose) {
580        boolean old = _caboose;
581        _caboose = caboose;
582        if (!old == caboose) {
583            setDirtyAndFirePropertyChange("car is caboose", old ? "true" : "false", caboose ? "true" : "false"); // NOI18N
584        }
585    }
586
587    public boolean isCaboose() {
588        return _caboose;
589    }
590
591    public void setUtility(boolean utility) {
592        boolean old = _utility;
593        _utility = utility;
594        if (!old == utility) {
595            setDirtyAndFirePropertyChange("car is utility", old ? "true" : "false", utility ? "true" : "false"); // NOI18N
596        }
597    }
598
599    public boolean isUtility() {
600        return _utility;
601    }
602
603    /**
604     * Used to determine if car is performing a local move. A local move is when
605     * a car is moved to a different track at the same location.
606     * 
607     * @return true if local move
608     */
609    public boolean isLocalMove() {
610        if (getTrain() == null && getLocation() != null) {
611            return getSplitLocationName().equals(getSplitDestinationName());
612        }
613        if (getRouteLocation() == null || getRouteDestination() == null) {
614            return false;
615        }
616        if (getRouteLocation().equals(getRouteDestination()) && getTrack() != null) {
617            return true;
618        }
619        if (getTrain().isLocalSwitcher() &&
620                getRouteLocation().getSplitName()
621                        .equals(getRouteDestination().getSplitName()) &&
622                getTrack() != null) {
623            return true;
624        }
625        // look for sequential locations with the "same" name
626        if (getRouteLocation().getSplitName().equals(
627                getRouteDestination().getSplitName()) && getTrain().getRoute() != null) {
628            boolean foundRl = false;
629            for (RouteLocation rl : getTrain().getRoute().getLocationsBySequenceList()) {
630                if (foundRl) {
631                    if (getRouteDestination().getSplitName()
632                            .equals(rl.getSplitName())) {
633                        // user can specify the "same" location two more more
634                        // times in a row
635                        if (getRouteDestination() != rl) {
636                            continue;
637                        } else {
638                            return true;
639                        }
640                    } else {
641                        return false;
642                    }
643                }
644                if (getRouteLocation().equals(rl)) {
645                    foundRl = true;
646                }
647            }
648        }
649        return false;
650    }
651
652    /**
653     * A kernel is a group of cars that are switched as a unit.
654     * 
655     * @param kernel The assigned Kernel for this car.
656     */
657    public void setKernel(Kernel kernel) {
658        if (_kernel == kernel) {
659            return;
660        }
661        String old = "";
662        if (_kernel != null) {
663            old = _kernel.getName();
664            _kernel.delete(this);
665        }
666        _kernel = kernel;
667        String newName = "";
668        if (_kernel != null) {
669            _kernel.add(this);
670            newName = _kernel.getName();
671        }
672        if (!old.equals(newName)) {
673            setDirtyAndFirePropertyChange(KERNEL_NAME_CHANGED_PROPERTY, old, newName); // NOI18N
674        }
675    }
676
677    public Kernel getKernel() {
678        return _kernel;
679    }
680
681    public String getKernelName() {
682        if (_kernel != null) {
683            return _kernel.getName();
684        }
685        return NONE;
686    }
687
688    /**
689     * Used to determine if car is lead car in a kernel
690     * 
691     * @return true if lead car in a kernel
692     */
693    public boolean isLead() {
694        if (getKernel() != null) {
695            return getKernel().isLead(this);
696        }
697        return false;
698    }
699
700    /**
701     * Updates all cars in a kernel. After the update, the cars will all have
702     * the same final destination, load, and route path.
703     */
704    public void updateKernel() {
705        if (isLead()) {
706            for (Car car : getKernel().getCars()) {
707                car.setScheduleItemId(getScheduleItemId());
708                car.setFinalDestination(getFinalDestination());
709                car.setFinalDestinationTrack(getFinalDestinationTrack());
710                car.setLoadGeneratedFromStaging(isLoadGeneratedFromStaging());
711                car.setRoutePath(getRoutePath());
712                if (InstanceManager.getDefault(CarLoads.class).containsName(car.getTypeName(), getLoadName())) {
713                    car.setLoadName(getLoadName());
714                }
715            }
716        }
717    }
718
719    /**
720     * Returns the car length or the length of the car's kernel including
721     * couplers.
722     * 
723     * @return length of car or kernel
724     */
725    public int getTotalKernelLength() {
726        if (getKernel() != null) {
727            return getKernel().getTotalLength();
728        }
729        return getTotalLength();
730    }
731
732    /**
733     * Used to determine if a car can be set out at a destination (location).
734     * Track is optional. In addition to all of the tests that checkDestination
735     * performs, spurs with schedules are also checked.
736     *
737     * @return status OKAY, TYPE, ROAD, LENGTH, ERROR_TRACK, CAPACITY, SCHEDULE,
738     *         CUSTOM
739     */
740    @Override
741    public String checkDestination(Location destination, Track track) {
742        String status = super.checkDestination(destination, track);
743        if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
744            return status;
745        }
746        // now check to see if the track has a schedule
747        if (track == null) {
748            return status;
749        }
750        String statusSchedule = track.checkSchedule(this);
751        if (status.startsWith(Track.LENGTH) && statusSchedule.equals(Track.OKAY)) {
752            return status;
753        }
754        return statusSchedule;
755    }
756
757    /**
758     * Sets the car's destination on the layout
759     *
760     * @param track (yard, spur, staging, or interchange track)
761     * @return "okay" if successful, "type" if the rolling stock's type isn't
762     *         acceptable, or "length" if the rolling stock length didn't fit,
763     *         or Schedule if the destination will not accept the car because
764     *         the spur has a schedule and the car doesn't meet the schedule
765     *         requirements. Also changes the car load status when the car
766     *         reaches its destination.
767     */
768    @Override
769    public String setDestination(Location destination, Track track) {
770        return setDestination(destination, track, false);
771    }
772
773    /**
774     * Sets the car's destination on the layout
775     *
776     * @param track (yard, spur, staging, or interchange track)
777     * @param force when true ignore track length, type, and road when setting
778     *              destination
779     * @return "okay" if successful, "type" if the rolling stock's type isn't
780     *         acceptable, or "length" if the rolling stock length didn't fit,
781     *         or Schedule if the destination will not accept the car because
782     *         the spur has a schedule and the car doesn't meet the schedule
783     *         requirements. Also changes the car load status when the car
784     *         reaches its destination.
785     */
786    @Override
787    public String setDestination(Location destination, Track track, boolean force) {
788        // save destination name and track in case car has reached its
789        // destination
790        String destinationName = getDestinationName();
791        Track destinationTrack = getDestinationTrack();
792        String status = super.setDestination(destination, track, force);
793        // return if not Okay
794        if (!status.equals(Track.OKAY)) {
795            return status;
796        }
797        // now check to see if the track has a schedule
798        if (track != null && destinationTrack != track && loaded) {
799            status = track.scheduleNext(this);
800            if (!status.equals(Track.OKAY)) {
801                return status;
802            }
803        }
804        // done?
805        if (destinationName.equals(NONE) || (destination != null) || getTrain() == null) {
806            return status;
807        }
808        // car was in a train and has been dropped off, update load, RWE could
809        // set a new final destination
810        loadNext(destinationTrack);
811        return status;
812    }
813
814    /**
815     * Called when setting a car's destination to this spur. Loads the car with
816     * a final destination which is the ship address for the schedule item.
817     * 
818     * @param scheduleItem The schedule item to be applied this this car
819     */
820    public void loadNext(ScheduleItem scheduleItem) {
821        if (scheduleItem == null) {
822            return; // should never be null
823        }
824        // set the car's final destination and track
825        setFinalDestination(scheduleItem.getDestination());
826        setFinalDestinationTrack(scheduleItem.getDestinationTrack());
827        // bump hit count for this schedule item
828        scheduleItem.setHits(scheduleItem.getHits() + 1);
829        // set all cars in kernel same final destination
830        updateKernel();
831    }
832
833    /**
834     * Called when car is delivered to track. Updates the car's wait, pickup
835     * day, and load if spur. If staging, can swap default loads, force load to
836     * default empty, or replace custom loads with the default empty load. Can
837     * trigger RWE or RWL.
838     * 
839     * @param track the destination track for this car
840     */
841    public void loadNext(Track track) {
842        setLoadGeneratedFromStaging(false);
843        if (track != null) {
844            if (track.isSpur()) {
845                ScheduleItem si = getScheduleItem(track);
846                if (si == null) {
847                    log.debug("Schedule item ({}) is null for car ({}) at spur ({})", getScheduleItemId(), toString(),
848                            track.getName());
849                } else {
850                    setWait(si.getWait());
851                    setPickupScheduleId(si.getPickupTrainScheduleId());
852                }
853                updateLoad(track);
854            }
855            // update load optionally when car reaches staging
856            else if (track.isStaging()) {
857                if (track.isLoadSwapEnabled() && getLoadName().equals(carLoads.getDefaultEmptyName())) {
858                    setLoadLoaded();
859                } else if ((track.isLoadSwapEnabled() || track.isLoadEmptyEnabled()) &&
860                        getLoadName().equals(carLoads.getDefaultLoadName())) {
861                    setLoadEmpty();
862                } else if (track.isRemoveCustomLoadsEnabled() &&
863                        !getLoadName().equals(carLoads.getDefaultEmptyName()) &&
864                        !getLoadName().equals(carLoads.getDefaultLoadName())) {
865                    // remove this car's final destination if it has one
866                    setFinalDestination(null);
867                    setFinalDestinationTrack(null);
868                    if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY) && isRwlEnabled()) {
869                        setLoadLoaded();
870                        // car arriving into staging with the RWE load?
871                    } else if (getLoadName().equals(getReturnWhenEmptyLoadName())) {
872                        setLoadName(carLoads.getDefaultEmptyName());
873                    } else {
874                        setLoadEmpty(); // note that RWE sets the car's final
875                                        // destination
876                    }
877                }
878            }
879        }
880    }
881
882    /**
883     * Updates a car's load when placed at a spur. Load change delayed if wait
884     * count is greater than zero. 
885     * 
886     * @param track The spur the car is sitting on
887     */
888    public void updateLoad(Track track) {
889        if (track.isDisableLoadChangeEnabled()) {
890            return;
891        }
892        if (getWait() > 0) {
893            return; // change load name when wait count reaches 0
894        }
895        // arriving at spur with a schedule?
896        String loadName = NONE;
897        ScheduleItem si = getScheduleItem(track);
898        if (si != null) {
899            loadName = si.getShipLoadName(); // can be NONE
900        } else {
901            // for backwards compatibility before version 5.1.4
902            log.debug("Schedule item ({}) is null for car ({}) at spur ({}), using next load name", getScheduleItemId(),
903                    toString(), track.getName());
904            loadName = getNextLoadName();
905        }
906        setNextLoadName(NONE);
907        if (!loadName.equals(NONE)) {
908            setLoadName(loadName);
909            // RWE or RWL load and no destination?
910            if (getLoadName().equals(getReturnWhenEmptyLoadName()) && getFinalDestination() == null) {
911                setReturnWhenEmpty();
912            } else if (getLoadName().equals(getReturnWhenLoadedLoadName()) && getFinalDestination() == null) {
913                setReturnWhenLoaded();
914            }
915        } else {
916            // flip load names
917            if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
918                setLoadLoaded();
919            } else {
920                setLoadEmpty();
921            }
922        }
923        setScheduleItemId(Car.NONE);
924    }
925
926    /**
927     * Sets the car's load to empty, triggers RWE load and destination if
928     * enabled.
929     */
930    private void setLoadEmpty() {
931        if (!getLoadName().equals(getReturnWhenEmptyLoadName())) {
932            setLoadName(getReturnWhenEmptyLoadName()); // default RWE load is
933                                                       // the "E" load
934            setReturnWhenEmpty();
935        }
936    }
937
938    /*
939     * Don't set return address if in staging with the same RWE address and
940     * don't set return address if at the RWE address
941     */
942    private void setReturnWhenEmpty() {
943        if (getReturnWhenEmptyDestination() != null &&
944                (getLocation() != getReturnWhenEmptyDestination() ||
945                        (!getReturnWhenEmptyDestination().isStaging() &&
946                                getTrack() != getReturnWhenEmptyDestTrack()))) {
947            setFinalDestination(getReturnWhenEmptyDestination());
948            if (getReturnWhenEmptyDestTrack() != null) {
949                setFinalDestinationTrack(getReturnWhenEmptyDestTrack());
950            }
951            log.debug("Car ({}) has return when empty destination ({}, {}) load {}", toString(),
952                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
953        }
954    }
955
956    /**
957     * Sets the car's load to loaded, triggers RWL load and destination if
958     * enabled.
959     */
960    private void setLoadLoaded() {
961        if (!getLoadName().equals(getReturnWhenLoadedLoadName())) {
962            setLoadName(getReturnWhenLoadedLoadName()); // default RWL load is
963                                                        // the "L" load
964            setReturnWhenLoaded();
965        }
966    }
967
968    /*
969     * Don't set return address if in staging with the same RWL address and
970     * don't set return address if at the RWL address
971     */
972    private void setReturnWhenLoaded() {
973        if (getReturnWhenLoadedDestination() != null &&
974                (getLocation() != getReturnWhenLoadedDestination() ||
975                        (!getReturnWhenLoadedDestination().isStaging() &&
976                                getTrack() != getReturnWhenLoadedDestTrack()))) {
977            setFinalDestination(getReturnWhenLoadedDestination());
978            if (getReturnWhenLoadedDestTrack() != null) {
979                setFinalDestinationTrack(getReturnWhenLoadedDestTrack());
980            }
981            log.debug("Car ({}) has return when loaded destination ({}, {}) load {}", toString(),
982                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
983        }
984    }
985
986    public String getTypeExtensions() {
987        StringBuffer buf = new StringBuffer();
988        if (isCaboose()) {
989            buf.append(EXTENSION_REGEX + CABOOSE_EXTENSION);
990        }
991        if (hasFred()) {
992            buf.append(EXTENSION_REGEX + FRED_EXTENSION);
993        }
994        if (isPassenger()) {
995            buf.append(EXTENSION_REGEX + PASSENGER_EXTENSION + EXTENSION_REGEX + getBlocking());
996        }
997        if (isUtility()) {
998            buf.append(EXTENSION_REGEX + UTILITY_EXTENSION);
999        }
1000        if (isCarHazardous()) {
1001            buf.append(EXTENSION_REGEX + HAZARDOUS_EXTENSION);
1002        }
1003        return buf.toString();
1004    }
1005
1006    @Override
1007    public void reset() {
1008        setScheduleItemId(getPreviousScheduleId()); // revert to previous
1009        setNextLoadName(NONE);
1010        setFinalDestination(getPreviousFinalDestination());
1011        setFinalDestinationTrack(getPreviousFinalDestinationTrack());
1012        if (isLoadGeneratedFromStaging()) {
1013            setLoadGeneratedFromStaging(false);
1014            setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
1015        }
1016        super.reset();
1017    }
1018
1019    @Override
1020    public void dispose() {
1021        setKernel(null);
1022        setFinalDestination(null); // removes property change listener
1023        setFinalDestinationTrack(null); // removes property change listener
1024        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
1025        InstanceManager.getDefault(CarLengths.class).removePropertyChangeListener(this);
1026        super.dispose();
1027    }
1028
1029    // used to stop a track's schedule from bumping when loading car database
1030    private boolean loaded = false;
1031
1032    /**
1033     * Construct this Entry from XML. This member has to remain synchronized
1034     * with the detailed DTD in operations-cars.dtd
1035     *
1036     * @param e Car XML element
1037     */
1038    public Car(org.jdom2.Element e) {
1039        super(e);
1040        loaded = true;
1041        org.jdom2.Attribute a;
1042        if ((a = e.getAttribute(Xml.PASSENGER)) != null) {
1043            _passenger = a.getValue().equals(Xml.TRUE);
1044        }
1045        if ((a = e.getAttribute(Xml.HAZARDOUS)) != null) {
1046            _hazardous = a.getValue().equals(Xml.TRUE);
1047        }
1048        if ((a = e.getAttribute(Xml.CABOOSE)) != null) {
1049            _caboose = a.getValue().equals(Xml.TRUE);
1050        }
1051        if ((a = e.getAttribute(Xml.FRED)) != null) {
1052            _fred = a.getValue().equals(Xml.TRUE);
1053        }
1054        if ((a = e.getAttribute(Xml.UTILITY)) != null) {
1055            _utility = a.getValue().equals(Xml.TRUE);
1056        }
1057        if ((a = e.getAttribute(Xml.KERNEL)) != null) {
1058            Kernel k = InstanceManager.getDefault(KernelManager.class).getKernelByName(a.getValue());
1059            if (k != null) {
1060                setKernel(k);
1061                if ((a = e.getAttribute(Xml.LEAD_KERNEL)) != null && a.getValue().equals(Xml.TRUE)) {
1062                    _kernel.setLead(this);
1063                }
1064            } else {
1065                log.error("Kernel {} does not exist", a.getValue());
1066            }
1067        }
1068        if ((a = e.getAttribute(Xml.LOAD)) != null) {
1069            _loadName = a.getValue();
1070        }
1071        if ((a = e.getAttribute(Xml.LOAD_FROM_STAGING)) != null && a.getValue().equals(Xml.TRUE)) {
1072            setLoadGeneratedFromStaging(true);
1073        }
1074        if ((a = e.getAttribute(Xml.WAIT)) != null) {
1075            try {
1076                _wait = Integer.parseInt(a.getValue());
1077            } catch (NumberFormatException nfe) {
1078                log.error("Wait count ({}) for car ({}) isn't a valid number!", a.getValue(), toString());
1079            }
1080        }
1081        if ((a = e.getAttribute(Xml.PICKUP_SCHEDULE_ID)) != null) {
1082            _pickupScheduleId = a.getValue();
1083        }
1084        if ((a = e.getAttribute(Xml.SCHEDULE_ID)) != null) {
1085            _scheduleId = a.getValue();
1086        }
1087        // for backwards compatibility before version 5.1.4
1088        if ((a = e.getAttribute(Xml.NEXT_LOAD)) != null) {
1089            _nextLoadName = a.getValue();
1090        }
1091        if ((a = e.getAttribute(Xml.NEXT_DEST_ID)) != null) {
1092            setFinalDestination(InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1093        }
1094        if (getFinalDestination() != null && (a = e.getAttribute(Xml.NEXT_DEST_TRACK_ID)) != null) {
1095            setFinalDestinationTrack(getFinalDestination().getTrackById(a.getValue()));
1096        }
1097        if ((a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_ID)) != null) {
1098            setPreviousFinalDestination(
1099                    InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1100        }
1101        if (getPreviousFinalDestination() != null && (a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID)) != null) {
1102            setPreviousFinalDestinationTrack(getPreviousFinalDestination().getTrackById(a.getValue()));
1103        }
1104        if ((a = e.getAttribute(Xml.PREVIOUS_SCHEDULE_ID)) != null) {
1105            setPreviousScheduleId(a.getValue());
1106        }
1107        if ((a = e.getAttribute(Xml.RWE_DEST_ID)) != null) {
1108            _rweDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1109        }
1110        if (_rweDestination != null && (a = e.getAttribute(Xml.RWE_DEST_TRACK_ID)) != null) {
1111            _rweDestTrack = _rweDestination.getTrackById(a.getValue());
1112        }
1113        if ((a = e.getAttribute(Xml.RWE_LOAD)) != null) {
1114            _rweLoadName = a.getValue();
1115        }
1116        if ((a = e.getAttribute(Xml.RWL_DEST_ID)) != null) {
1117            _rwlDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1118        }
1119        if (_rwlDestination != null && (a = e.getAttribute(Xml.RWL_DEST_TRACK_ID)) != null) {
1120            _rwlDestTrack = _rwlDestination.getTrackById(a.getValue());
1121        }
1122        if ((a = e.getAttribute(Xml.RWL_LOAD)) != null) {
1123            _rwlLoadName = a.getValue();
1124        }
1125        if ((a = e.getAttribute(Xml.ROUTE_PATH)) != null) {
1126            _routePath = a.getValue();
1127        }
1128        addPropertyChangeListeners();
1129    }
1130
1131    /**
1132     * Create an XML element to represent this Entry. This member has to remain
1133     * synchronized with the detailed DTD in operations-cars.dtd.
1134     *
1135     * @return Contents in a JDOM Element
1136     */
1137    public org.jdom2.Element store() {
1138        org.jdom2.Element e = new org.jdom2.Element(Xml.CAR);
1139        super.store(e);
1140        if (isPassenger()) {
1141            e.setAttribute(Xml.PASSENGER, isPassenger() ? Xml.TRUE : Xml.FALSE);
1142        }
1143        if (isCarHazardous()) {
1144            e.setAttribute(Xml.HAZARDOUS, isCarHazardous() ? Xml.TRUE : Xml.FALSE);
1145        }
1146        if (isCaboose()) {
1147            e.setAttribute(Xml.CABOOSE, isCaboose() ? Xml.TRUE : Xml.FALSE);
1148        }
1149        if (hasFred()) {
1150            e.setAttribute(Xml.FRED, hasFred() ? Xml.TRUE : Xml.FALSE);
1151        }
1152        if (isUtility()) {
1153            e.setAttribute(Xml.UTILITY, isUtility() ? Xml.TRUE : Xml.FALSE);
1154        }
1155        if (getKernel() != null) {
1156            e.setAttribute(Xml.KERNEL, getKernelName());
1157            if (isLead()) {
1158                e.setAttribute(Xml.LEAD_KERNEL, Xml.TRUE);
1159            }
1160        }
1161
1162        e.setAttribute(Xml.LOAD, getLoadName());
1163
1164        if (isLoadGeneratedFromStaging()) {
1165            e.setAttribute(Xml.LOAD_FROM_STAGING, Xml.TRUE);
1166        }
1167
1168        if (getWait() != 0) {
1169            e.setAttribute(Xml.WAIT, Integer.toString(getWait()));
1170        }
1171
1172        if (!getPickupScheduleId().equals(NONE)) {
1173            e.setAttribute(Xml.PICKUP_SCHEDULE_ID, getPickupScheduleId());
1174        }
1175
1176        if (!getScheduleItemId().equals(NONE)) {
1177            e.setAttribute(Xml.SCHEDULE_ID, getScheduleItemId());
1178        }
1179
1180        // for backwards compatibility before version 5.1.4
1181        if (!getNextLoadName().equals(NONE)) {
1182            e.setAttribute(Xml.NEXT_LOAD, getNextLoadName());
1183        }
1184
1185        if (getFinalDestination() != null) {
1186            e.setAttribute(Xml.NEXT_DEST_ID, getFinalDestination().getId());
1187            if (getFinalDestinationTrack() != null) {
1188                e.setAttribute(Xml.NEXT_DEST_TRACK_ID, getFinalDestinationTrack().getId());
1189            }
1190        }
1191
1192        if (getPreviousFinalDestination() != null) {
1193            e.setAttribute(Xml.PREVIOUS_NEXT_DEST_ID, getPreviousFinalDestination().getId());
1194            if (getPreviousFinalDestinationTrack() != null) {
1195                e.setAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID, getPreviousFinalDestinationTrack().getId());
1196            }
1197        }
1198
1199        if (!getPreviousScheduleId().equals(NONE)) {
1200            e.setAttribute(Xml.PREVIOUS_SCHEDULE_ID, getPreviousScheduleId());
1201        }
1202
1203        if (getReturnWhenEmptyDestination() != null) {
1204            e.setAttribute(Xml.RWE_DEST_ID, getReturnWhenEmptyDestination().getId());
1205            if (getReturnWhenEmptyDestTrack() != null) {
1206                e.setAttribute(Xml.RWE_DEST_TRACK_ID, getReturnWhenEmptyDestTrack().getId());
1207            }
1208        }
1209        if (!getReturnWhenEmptyLoadName().equals(carLoads.getDefaultEmptyName())) {
1210            e.setAttribute(Xml.RWE_LOAD, getReturnWhenEmptyLoadName());
1211        }
1212
1213        if (getReturnWhenLoadedDestination() != null) {
1214            e.setAttribute(Xml.RWL_DEST_ID, getReturnWhenLoadedDestination().getId());
1215            if (getReturnWhenLoadedDestTrack() != null) {
1216                e.setAttribute(Xml.RWL_DEST_TRACK_ID, getReturnWhenLoadedDestTrack().getId());
1217            }
1218        }
1219        if (!getReturnWhenLoadedLoadName().equals(carLoads.getDefaultLoadName())) {
1220            e.setAttribute(Xml.RWL_LOAD, getReturnWhenLoadedLoadName());
1221        }
1222
1223        if (!getRoutePath().isEmpty()) {
1224            e.setAttribute(Xml.ROUTE_PATH, getRoutePath());
1225        }
1226
1227        return e;
1228    }
1229
1230    @Override
1231    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1232        // Set dirty
1233        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
1234        super.setDirtyAndFirePropertyChange(p, old, n);
1235    }
1236
1237    private void addPropertyChangeListeners() {
1238        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
1239        InstanceManager.getDefault(CarLengths.class).addPropertyChangeListener(this);
1240    }
1241
1242    @Override
1243    public void propertyChange(PropertyChangeEvent e) {
1244        super.propertyChange(e);
1245        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY)) {
1246            if (e.getOldValue().equals(getTypeName())) {
1247                log.debug("Car ({}) sees type name change old: ({}) new: ({})", toString(), e.getOldValue(),
1248                        e.getNewValue()); // NOI18N
1249                setTypeName((String) e.getNewValue());
1250            }
1251        }
1252        if (e.getPropertyName().equals(CarLengths.CARLENGTHS_NAME_CHANGED_PROPERTY)) {
1253            if (e.getOldValue().equals(getLength())) {
1254                log.debug("Car ({}) sees length name change old: ({}) new: ({})", toString(), e.getOldValue(),
1255                        e.getNewValue()); // NOI18N
1256                setLength((String) e.getNewValue());
1257            }
1258        }
1259        if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) {
1260            if (e.getSource() == getFinalDestination()) {
1261                log.debug("delete final destination for car: ({})", toString());
1262                setFinalDestination(null);
1263            }
1264        }
1265        if (e.getPropertyName().equals(Track.DISPOSE_CHANGED_PROPERTY)) {
1266            if (e.getSource() == getFinalDestinationTrack()) {
1267                log.debug("delete final destination for car: ({})", toString());
1268                setFinalDestinationTrack(null);
1269            }
1270        }
1271    }
1272
1273    private final static Logger log = LoggerFactory.getLogger(Car.class);
1274
1275}