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 route path.
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                car.setRoutePath(getRoutePath());
696                if (InstanceManager.getDefault(CarLoads.class).containsName(car.getTypeName(), getLoadName())) {
697                    car.setLoadName(getLoadName());
698                }
699            }
700        }
701    }
702
703    /**
704     * Used to determine if a car can be set out at a destination (location).
705     * Track is optional. In addition to all of the tests that checkDestination
706     * performs, spurs with schedules are also checked.
707     *
708     * @return status OKAY, TYPE, ROAD, LENGTH, ERROR_TRACK, CAPACITY, SCHEDULE,
709     *         CUSTOM
710     */
711    @Override
712    public String checkDestination(Location destination, Track track) {
713        String status = super.checkDestination(destination, track);
714        if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
715            return status;
716        }
717        // now check to see if the track has a schedule
718        if (track == null) {
719            return status;
720        }
721        String statusSchedule = track.checkSchedule(this);
722        if (status.startsWith(Track.LENGTH) && statusSchedule.equals(Track.OKAY)) {
723            return status;
724        }
725        return statusSchedule;
726    }
727
728    /**
729     * Sets the car's destination on the layout
730     *
731     * @param track (yard, spur, staging, or interchange track)
732     * @return "okay" if successful, "type" if the rolling stock's type isn't
733     *         acceptable, or "length" if the rolling stock length didn't fit,
734     *         or Schedule if the destination will not accept the car because
735     *         the spur has a schedule and the car doesn't meet the schedule
736     *         requirements. Also changes the car load status when the car
737     *         reaches its destination.
738     */
739    @Override
740    public String setDestination(Location destination, Track track) {
741        return setDestination(destination, track, false);
742    }
743
744    /**
745     * Sets the car's destination on the layout
746     *
747     * @param track (yard, spur, staging, or interchange track)
748     * @param force when true ignore track length, type, and road when setting
749     *              destination
750     * @return "okay" if successful, "type" if the rolling stock's type isn't
751     *         acceptable, or "length" if the rolling stock length didn't fit,
752     *         or Schedule if the destination will not accept the car because
753     *         the spur has a schedule and the car doesn't meet the schedule
754     *         requirements. Also changes the car load status when the car
755     *         reaches its destination.
756     */
757    @Override
758    public String setDestination(Location destination, Track track, boolean force) {
759        // save destination name and track in case car has reached its
760        // destination
761        String destinationName = getDestinationName();
762        Track destinationTrack = getDestinationTrack();
763        String status = super.setDestination(destination, track, force);
764        // return if not Okay
765        if (!status.equals(Track.OKAY)) {
766            return status;
767        }
768        // now check to see if the track has a schedule
769        if (track != null && destinationTrack != track && loaded) {
770            status = track.scheduleNext(this);
771            if (!status.equals(Track.OKAY)) {
772                return status;
773            }
774        }
775        // done?
776        if (destinationName.equals(NONE) || (destination != null) || getTrain() == null) {
777            return status;
778        }
779        // car was in a train and has been dropped off, update load, RWE could
780        // set a new final destination
781        loadNext(destinationTrack);
782        return status;
783    }
784
785    /**
786     * Called when setting a car's destination to this spur. Loads the car with
787     * a final destination which is the ship address for the schedule item.
788     * 
789     * @param scheduleItem The schedule item to be applied this this car
790     */
791    public void loadNext(ScheduleItem scheduleItem) {
792        if (scheduleItem == null) {
793            return; // should never be null
794        }
795        // set the car's final destination and track
796        setFinalDestination(scheduleItem.getDestination());
797        setFinalDestinationTrack(scheduleItem.getDestinationTrack());
798        // bump hit count for this schedule item
799        scheduleItem.setHits(scheduleItem.getHits() + 1);
800        // set all cars in kernel same final destination
801        updateKernel();
802    }
803
804    /**
805     * Called when car is delivered to track. Updates the car's wait, pickup
806     * day, and load if spur. If staging, can swap default loads, force load to
807     * default empty, or replace custom loads with the default empty load. Can
808     * trigger RWE or RWL.
809     * 
810     * @param track the destination track for this car
811     */
812    public void loadNext(Track track) {
813        setLoadGeneratedFromStaging(false);
814        if (track != null) {
815            if (track.isSpur()) {
816                ScheduleItem si = getScheduleItem(track);
817                if (si == null) {
818                    log.debug("Schedule item ({}) is null for car ({}) at spur ({})", getScheduleItemId(), toString(),
819                            track.getName());
820                } else {
821                    setWait(si.getWait());
822                    setPickupScheduleId(si.getPickupTrainScheduleId());
823                }
824                updateLoad(track);
825            }
826            // update load optionally when car reaches staging
827            else if (track.isStaging()) {
828                if (track.isLoadSwapEnabled() && getLoadName().equals(carLoads.getDefaultEmptyName())) {
829                    setLoadLoaded();
830                } else if ((track.isLoadSwapEnabled() || track.isLoadEmptyEnabled()) &&
831                        getLoadName().equals(carLoads.getDefaultLoadName())) {
832                    setLoadEmpty();
833                } else if (track.isRemoveCustomLoadsEnabled() &&
834                        !getLoadName().equals(carLoads.getDefaultEmptyName()) &&
835                        !getLoadName().equals(carLoads.getDefaultLoadName())) {
836                    // remove this car's final destination if it has one
837                    setFinalDestination(null);
838                    setFinalDestinationTrack(null);
839                    if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY) && isRwlEnabled()) {
840                        setLoadLoaded();
841                        // car arriving into staging with the RWE load?
842                    } else if (getLoadName().equals(getReturnWhenEmptyLoadName())) {
843                        setLoadName(carLoads.getDefaultEmptyName());
844                    } else {
845                        setLoadEmpty(); // note that RWE sets the car's final
846                                        // destination
847                    }
848                }
849            }
850        }
851    }
852
853    /**
854     * Updates a car's load when placed at a spur. Load change delayed if wait
855     * count is greater than zero. 
856     * 
857     * @param track The spur the car is sitting on
858     */
859    public void updateLoad(Track track) {
860        if (track.isDisableLoadChangeEnabled()) {
861            return;
862        }
863        if (getWait() > 0) {
864            return; // change load name when wait count reaches 0
865        }
866        // arriving at spur with a schedule?
867        String loadName = NONE;
868        ScheduleItem si = getScheduleItem(track);
869        if (si != null) {
870            loadName = si.getShipLoadName(); // can be NONE
871        } else {
872            // for backwards compatibility before version 5.1.4
873            log.debug("Schedule item ({}) is null for car ({}) at spur ({}), using next load name", getScheduleItemId(),
874                    toString(), track.getName());
875            loadName = getNextLoadName();
876        }
877        setNextLoadName(NONE);
878        if (!loadName.equals(NONE)) {
879            setLoadName(loadName);
880            // RWE or RWL load and no destination?
881            if (getLoadName().equals(getReturnWhenEmptyLoadName()) && getFinalDestination() == null) {
882                setReturnWhenEmpty();
883            } else if (getLoadName().equals(getReturnWhenLoadedLoadName()) && getFinalDestination() == null) {
884                setReturnWhenLoaded();
885            }
886        } else {
887            // flip load names
888            if (getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY)) {
889                setLoadLoaded();
890            } else {
891                setLoadEmpty();
892            }
893        }
894        setScheduleItemId(Car.NONE);
895    }
896
897    /**
898     * Sets the car's load to empty, triggers RWE load and destination if
899     * enabled.
900     */
901    private void setLoadEmpty() {
902        if (!getLoadName().equals(getReturnWhenEmptyLoadName())) {
903            setLoadName(getReturnWhenEmptyLoadName()); // default RWE load is
904                                                       // the "E" load
905            setReturnWhenEmpty();
906        }
907    }
908
909    /*
910     * Don't set return address if in staging with the same RWE address and
911     * don't set return address if at the RWE address
912     */
913    private void setReturnWhenEmpty() {
914        if (getReturnWhenEmptyDestination() != null &&
915                (getLocation() != getReturnWhenEmptyDestination() ||
916                        (!getReturnWhenEmptyDestination().isStaging() &&
917                                getTrack() != getReturnWhenEmptyDestTrack()))) {
918            setFinalDestination(getReturnWhenEmptyDestination());
919            if (getReturnWhenEmptyDestTrack() != null) {
920                setFinalDestinationTrack(getReturnWhenEmptyDestTrack());
921            }
922            log.debug("Car ({}) has return when empty destination ({}, {}) load {}", toString(),
923                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
924        }
925    }
926
927    /**
928     * Sets the car's load to loaded, triggers RWL load and destination if
929     * enabled.
930     */
931    private void setLoadLoaded() {
932        if (!getLoadName().equals(getReturnWhenLoadedLoadName())) {
933            setLoadName(getReturnWhenLoadedLoadName()); // default RWL load is
934                                                        // the "L" load
935            setReturnWhenLoaded();
936        }
937    }
938
939    /*
940     * Don't set return address if in staging with the same RWL address and
941     * don't set return address if at the RWL address
942     */
943    private void setReturnWhenLoaded() {
944        if (getReturnWhenLoadedDestination() != null &&
945                (getLocation() != getReturnWhenLoadedDestination() ||
946                        (!getReturnWhenLoadedDestination().isStaging() &&
947                                getTrack() != getReturnWhenLoadedDestTrack()))) {
948            setFinalDestination(getReturnWhenLoadedDestination());
949            if (getReturnWhenLoadedDestTrack() != null) {
950                setFinalDestinationTrack(getReturnWhenLoadedDestTrack());
951            }
952            log.debug("Car ({}) has return when loaded destination ({}, {}) load {}", toString(),
953                    getFinalDestinationName(), getFinalDestinationTrackName(), getLoadName());
954        }
955    }
956
957    public String getTypeExtensions() {
958        StringBuffer buf = new StringBuffer();
959        if (isCaboose()) {
960            buf.append(EXTENSION_REGEX + CABOOSE_EXTENSION);
961        }
962        if (hasFred()) {
963            buf.append(EXTENSION_REGEX + FRED_EXTENSION);
964        }
965        if (isPassenger()) {
966            buf.append(EXTENSION_REGEX + PASSENGER_EXTENSION + EXTENSION_REGEX + getBlocking());
967        }
968        if (isUtility()) {
969            buf.append(EXTENSION_REGEX + UTILITY_EXTENSION);
970        }
971        if (isCarHazardous()) {
972            buf.append(EXTENSION_REGEX + HAZARDOUS_EXTENSION);
973        }
974        return buf.toString();
975    }
976
977    @Override
978    public void reset() {
979        setScheduleItemId(getPreviousScheduleId()); // revert to previous
980        setNextLoadName(NONE);
981        setFinalDestination(getPreviousFinalDestination());
982        setFinalDestinationTrack(getPreviousFinalDestinationTrack());
983        if (isLoadGeneratedFromStaging()) {
984            setLoadGeneratedFromStaging(false);
985            setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
986        }
987        super.reset();
988    }
989
990    @Override
991    public void dispose() {
992        setKernel(null);
993        setFinalDestination(null); // removes property change listener
994        setFinalDestinationTrack(null); // removes property change listener
995        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
996        InstanceManager.getDefault(CarLengths.class).removePropertyChangeListener(this);
997        super.dispose();
998    }
999
1000    // used to stop a track's schedule from bumping when loading car database
1001    private boolean loaded = false;
1002
1003    /**
1004     * Construct this Entry from XML. This member has to remain synchronized
1005     * with the detailed DTD in operations-cars.dtd
1006     *
1007     * @param e Car XML element
1008     */
1009    public Car(org.jdom2.Element e) {
1010        super(e);
1011        loaded = true;
1012        org.jdom2.Attribute a;
1013        if ((a = e.getAttribute(Xml.PASSENGER)) != null) {
1014            _passenger = a.getValue().equals(Xml.TRUE);
1015        }
1016        if ((a = e.getAttribute(Xml.HAZARDOUS)) != null) {
1017            _hazardous = a.getValue().equals(Xml.TRUE);
1018        }
1019        if ((a = e.getAttribute(Xml.CABOOSE)) != null) {
1020            _caboose = a.getValue().equals(Xml.TRUE);
1021        }
1022        if ((a = e.getAttribute(Xml.FRED)) != null) {
1023            _fred = a.getValue().equals(Xml.TRUE);
1024        }
1025        if ((a = e.getAttribute(Xml.UTILITY)) != null) {
1026            _utility = a.getValue().equals(Xml.TRUE);
1027        }
1028        if ((a = e.getAttribute(Xml.KERNEL)) != null) {
1029            Kernel k = InstanceManager.getDefault(KernelManager.class).getKernelByName(a.getValue());
1030            if (k != null) {
1031                setKernel(k);
1032                if ((a = e.getAttribute(Xml.LEAD_KERNEL)) != null && a.getValue().equals(Xml.TRUE)) {
1033                    _kernel.setLead(this);
1034                }
1035            } else {
1036                log.error("Kernel {} does not exist", a.getValue());
1037            }
1038        }
1039        if ((a = e.getAttribute(Xml.LOAD)) != null) {
1040            _loadName = a.getValue();
1041        }
1042        if ((a = e.getAttribute(Xml.LOAD_FROM_STAGING)) != null && a.getValue().equals(Xml.TRUE)) {
1043            setLoadGeneratedFromStaging(true);
1044        }
1045
1046        if ((a = e.getAttribute(Xml.WAIT)) != null) {
1047            try {
1048                _wait = Integer.parseInt(a.getValue());
1049            } catch (NumberFormatException nfe) {
1050                log.error("Wait count ({}) for car ({}) isn't a valid number!", a.getValue(), toString());
1051            }
1052        }
1053        if ((a = e.getAttribute(Xml.PICKUP_SCHEDULE_ID)) != null) {
1054            _pickupScheduleId = a.getValue();
1055        }
1056        if ((a = e.getAttribute(Xml.SCHEDULE_ID)) != null) {
1057            _scheduleId = a.getValue();
1058        }
1059        // for backwards compatibility before version 5.1.4
1060        if ((a = e.getAttribute(Xml.NEXT_LOAD)) != null) {
1061            _nextLoadName = a.getValue();
1062        }
1063        if ((a = e.getAttribute(Xml.NEXT_DEST_ID)) != null) {
1064            setFinalDestination(InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1065        }
1066        if (getFinalDestination() != null && (a = e.getAttribute(Xml.NEXT_DEST_TRACK_ID)) != null) {
1067            setFinalDestinationTrack(getFinalDestination().getTrackById(a.getValue()));
1068        }
1069        if ((a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_ID)) != null) {
1070            setPreviousFinalDestination(
1071                    InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()));
1072        }
1073        if (getPreviousFinalDestination() != null && (a = e.getAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID)) != null) {
1074            setPreviousFinalDestinationTrack(getPreviousFinalDestination().getTrackById(a.getValue()));
1075        }
1076        if ((a = e.getAttribute(Xml.PREVIOUS_SCHEDULE_ID)) != null) {
1077            setPreviousScheduleId(a.getValue());
1078        }
1079        if ((a = e.getAttribute(Xml.RWE_DEST_ID)) != null) {
1080            _rweDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1081        }
1082        if (_rweDestination != null && (a = e.getAttribute(Xml.RWE_DEST_TRACK_ID)) != null) {
1083            _rweDestTrack = _rweDestination.getTrackById(a.getValue());
1084        }
1085        if ((a = e.getAttribute(Xml.RWE_LOAD)) != null) {
1086            _rweLoadName = a.getValue();
1087        }
1088        if ((a = e.getAttribute(Xml.RWL_DEST_ID)) != null) {
1089            _rwlDestination = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
1090        }
1091        if (_rwlDestination != null && (a = e.getAttribute(Xml.RWL_DEST_TRACK_ID)) != null) {
1092            _rwlDestTrack = _rwlDestination.getTrackById(a.getValue());
1093        }
1094        if ((a = e.getAttribute(Xml.RWL_LOAD)) != null) {
1095            _rwlLoadName = a.getValue();
1096        }
1097        if ((a = e.getAttribute(Xml.ROUTE_PATH)) != null) {
1098            _routePath = a.getValue();
1099        }
1100        addPropertyChangeListeners();
1101    }
1102
1103    /**
1104     * Create an XML element to represent this Entry. This member has to remain
1105     * synchronized with the detailed DTD in operations-cars.dtd.
1106     *
1107     * @return Contents in a JDOM Element
1108     */
1109    public org.jdom2.Element store() {
1110        org.jdom2.Element e = new org.jdom2.Element(Xml.CAR);
1111        super.store(e);
1112        if (isPassenger()) {
1113            e.setAttribute(Xml.PASSENGER, isPassenger() ? Xml.TRUE : Xml.FALSE);
1114        }
1115        if (isCarHazardous()) {
1116            e.setAttribute(Xml.HAZARDOUS, isCarHazardous() ? Xml.TRUE : Xml.FALSE);
1117        }
1118        if (isCaboose()) {
1119            e.setAttribute(Xml.CABOOSE, isCaboose() ? Xml.TRUE : Xml.FALSE);
1120        }
1121        if (hasFred()) {
1122            e.setAttribute(Xml.FRED, hasFred() ? Xml.TRUE : Xml.FALSE);
1123        }
1124        if (isUtility()) {
1125            e.setAttribute(Xml.UTILITY, isUtility() ? Xml.TRUE : Xml.FALSE);
1126        }
1127        if (getKernel() != null) {
1128            e.setAttribute(Xml.KERNEL, getKernelName());
1129            if (isLead()) {
1130                e.setAttribute(Xml.LEAD_KERNEL, Xml.TRUE);
1131            }
1132        }
1133
1134        e.setAttribute(Xml.LOAD, getLoadName());
1135
1136        if (isLoadGeneratedFromStaging()) {
1137            e.setAttribute(Xml.LOAD_FROM_STAGING, Xml.TRUE);
1138        }
1139
1140        if (getWait() != 0) {
1141            e.setAttribute(Xml.WAIT, Integer.toString(getWait()));
1142        }
1143
1144        if (!getPickupScheduleId().equals(NONE)) {
1145            e.setAttribute(Xml.PICKUP_SCHEDULE_ID, getPickupScheduleId());
1146        }
1147
1148        if (!getScheduleItemId().equals(NONE)) {
1149            e.setAttribute(Xml.SCHEDULE_ID, getScheduleItemId());
1150        }
1151
1152        // for backwards compatibility before version 5.1.4
1153        if (!getNextLoadName().equals(NONE)) {
1154            e.setAttribute(Xml.NEXT_LOAD, getNextLoadName());
1155        }
1156
1157        if (getFinalDestination() != null) {
1158            e.setAttribute(Xml.NEXT_DEST_ID, getFinalDestination().getId());
1159            if (getFinalDestinationTrack() != null) {
1160                e.setAttribute(Xml.NEXT_DEST_TRACK_ID, getFinalDestinationTrack().getId());
1161            }
1162        }
1163
1164        if (getPreviousFinalDestination() != null) {
1165            e.setAttribute(Xml.PREVIOUS_NEXT_DEST_ID, getPreviousFinalDestination().getId());
1166            if (getPreviousFinalDestinationTrack() != null) {
1167                e.setAttribute(Xml.PREVIOUS_NEXT_DEST_TRACK_ID, getPreviousFinalDestinationTrack().getId());
1168            }
1169        }
1170
1171        if (!getPreviousScheduleId().equals(NONE)) {
1172            e.setAttribute(Xml.PREVIOUS_SCHEDULE_ID, getPreviousScheduleId());
1173        }
1174
1175        if (getReturnWhenEmptyDestination() != null) {
1176            e.setAttribute(Xml.RWE_DEST_ID, getReturnWhenEmptyDestination().getId());
1177            if (getReturnWhenEmptyDestTrack() != null) {
1178                e.setAttribute(Xml.RWE_DEST_TRACK_ID, getReturnWhenEmptyDestTrack().getId());
1179            }
1180        }
1181        if (!getReturnWhenEmptyLoadName().equals(carLoads.getDefaultEmptyName())) {
1182            e.setAttribute(Xml.RWE_LOAD, getReturnWhenEmptyLoadName());
1183        }
1184
1185        if (getReturnWhenLoadedDestination() != null) {
1186            e.setAttribute(Xml.RWL_DEST_ID, getReturnWhenLoadedDestination().getId());
1187            if (getReturnWhenLoadedDestTrack() != null) {
1188                e.setAttribute(Xml.RWL_DEST_TRACK_ID, getReturnWhenLoadedDestTrack().getId());
1189            }
1190        }
1191        if (!getReturnWhenLoadedLoadName().equals(carLoads.getDefaultLoadName())) {
1192            e.setAttribute(Xml.RWL_LOAD, getReturnWhenLoadedLoadName());
1193        }
1194
1195        if (!getRoutePath().isEmpty()) {
1196            e.setAttribute(Xml.ROUTE_PATH, getRoutePath());
1197        }
1198
1199        return e;
1200    }
1201
1202    @Override
1203    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1204        // Set dirty
1205        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
1206        super.setDirtyAndFirePropertyChange(p, old, n);
1207    }
1208
1209    private void addPropertyChangeListeners() {
1210        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
1211        InstanceManager.getDefault(CarLengths.class).addPropertyChangeListener(this);
1212    }
1213
1214    @Override
1215    public void propertyChange(PropertyChangeEvent e) {
1216        super.propertyChange(e);
1217        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY)) {
1218            if (e.getOldValue().equals(getTypeName())) {
1219                log.debug("Car ({}) sees type name change old: ({}) new: ({})", toString(), e.getOldValue(),
1220                        e.getNewValue()); // NOI18N
1221                setTypeName((String) e.getNewValue());
1222            }
1223        }
1224        if (e.getPropertyName().equals(CarLengths.CARLENGTHS_NAME_CHANGED_PROPERTY)) {
1225            if (e.getOldValue().equals(getLength())) {
1226                log.debug("Car ({}) sees length name change old: ({}) new: ({})", toString(), e.getOldValue(),
1227                        e.getNewValue()); // NOI18N
1228                setLength((String) e.getNewValue());
1229            }
1230        }
1231        if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) {
1232            if (e.getSource() == getFinalDestination()) {
1233                log.debug("delete final destination for car: ({})", toString());
1234                setFinalDestination(null);
1235            }
1236        }
1237        if (e.getPropertyName().equals(Track.DISPOSE_CHANGED_PROPERTY)) {
1238            if (e.getSource() == getFinalDestinationTrack()) {
1239                log.debug("delete final destination for car: ({})", toString());
1240                setFinalDestinationTrack(null);
1241            }
1242        }
1243    }
1244
1245    private final static Logger log = LoggerFactory.getLogger(Car.class);
1246
1247}