001package jmri.jmrit.operations.rollingstock.cars;
002
003import java.beans.PropertyChangeEvent;
004import java.text.NumberFormat;
005import java.time.LocalDateTime;
006import java.util.*;
007
008import jmri.*;
009import jmri.jmrit.operations.locations.Track;
010import jmri.jmrit.operations.rollingstock.RollingStockManager;
011import jmri.jmrit.operations.routes.Route;
012import jmri.jmrit.operations.routes.RouteLocation;
013import jmri.jmrit.operations.setup.OperationsSetupXml;
014import jmri.jmrit.operations.setup.Setup;
015import jmri.jmrit.operations.trains.Train;
016import jmri.jmrit.operations.trains.TrainManifestHeaderText;
017
018import org.jdom2.Element;
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 * Manages the cars.
024 *
025 * @author Daniel Boudreau Copyright (C) 2008, 2026
026 */
027public class CarManager extends RollingStockManager<Car> implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize {
028
029    public CarManager() {
030    }
031
032    /**
033     * Finds an existing Car or creates a new Car if needed requires car's road
034     * and number
035     *
036     * @param road   car road
037     * @param number car number
038     * @return new car or existing Car
039     */
040    @Override
041    public Car newRS(String road, String number) {
042        Car car = getByRoadAndNumber(road, number);
043        if (car == null) {
044            car = new Car(road, number);
045            register(car);
046        }
047        return car;
048    }
049
050    @Override
051    public void deregister(Car car) {
052        super.deregister(car);
053        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
054    }
055
056    /**
057     * Sort by rolling stock location
058     *
059     * @return list of cars ordered by the Car's location
060     */
061    @Override
062    public List<Car> getByLocationList() {
063        List<Car> byFinal = getByList(getByNumberList(), BY_FINAL_DEST);
064        List<Car> byKernel = getByList(byFinal, BY_KERNEL);
065        return getByList(byKernel, BY_LOCATION);
066    }
067
068    /**
069     * Sort by car kernel names
070     *
071     * @return list of cars ordered by car kernel
072     */
073    public List<Car> getByKernelList() {
074        return getByList(getByList(getByNumberList(), BY_BLOCKING), BY_KERNEL);
075    }
076
077    /**
078     * Sort by car loads
079     *
080     * @return list of cars ordered by car loads
081     */
082    public List<Car> getByLoadList() {
083        return getByList(getByLocationList(), BY_LOAD);
084    }
085
086    /**
087     * Sort by car return when empty location and track
088     *
089     * @return list of cars ordered by car return when empty
090     */
091    public List<Car> getByRweList() {
092        return getByList(getByLocationList(), BY_RWE);
093    }
094
095    public List<Car> getByRwlList() {
096        return getByList(getByLocationList(), BY_RWL);
097    }
098
099    public List<Car> getByRouteList() {
100        return getByList(getByLocationList(), BY_ROUTE);
101    }
102
103    public List<Car> getByDivisionList() {
104        return getByList(getByLocationList(), BY_DIVISION);
105    }
106
107    public List<Car> getByFinalDestinationList() {
108        return getByList(getByDestinationList(), BY_FINAL_DEST);
109    }
110
111    /**
112     * Sort by car wait count
113     *
114     * @return list of cars ordered by wait count
115     */
116    public List<Car> getByWaitList() {
117        return getByList(getByIdList(), BY_WAIT);
118    }
119
120    @Override
121    public List<Car> getByPickupList() {
122        return getByList(getByDestinationList(), BY_PICKUP);
123    }
124
125    // The special sort options for cars
126    private static final int BY_LOAD_TYPE = 29;
127    private static final int BY_LOAD = 30;
128    private static final int BY_KERNEL = 31;
129    private static final int BY_RWE = 32; // Return When Empty
130    private static final int BY_FINAL_DEST = 33;
131    private static final int BY_WAIT = 34;
132    private static final int BY_PICKUP = 35;
133    private static final int BY_HAZARD = 36;
134    private static final int BY_RWL = 37; // Return When loaded
135    private static final int BY_ROUTE = 38;
136    private static final int BY_DIVISION = 39;
137
138    // the name of the location and track is "split"
139    private static final int BY_SPLIT_FINAL_DEST = 40;
140    private static final int BY_SPLIT_LOCATION = 41;
141    private static final int BY_SPLIT_DESTINATION = 42;
142
143    @Override
144    protected java.util.Comparator<Car> getComparator(int attribute) {
145        switch (attribute) {
146            case BY_LOAD_TYPE:
147                // load types "Empty" and "Load", load goes before empty
148                return (c1, c2) -> (c2.getLoadType().compareToIgnoreCase(c1.getLoadType()));
149            case BY_LOAD:
150                return (c1, c2) -> (c1.getLoadName().compareToIgnoreCase(c2.getLoadName()));
151            case BY_KERNEL:
152                return (c1, c2) -> (c1.getKernelName().compareToIgnoreCase(c2.getKernelName()));
153            case BY_RWE:
154                return (c1, c2) -> (c1.getReturnWhenEmptyDestinationName() + c1.getReturnWhenEmptyDestTrackName())
155                        .compareToIgnoreCase(
156                                c2.getReturnWhenEmptyDestinationName() + c2.getReturnWhenEmptyDestTrackName());
157            case BY_RWL:
158                return (c1, c2) -> (c1.getReturnWhenLoadedDestinationName() + c1.getReturnWhenLoadedDestTrackName())
159                        .compareToIgnoreCase(
160                                c2.getReturnWhenLoadedDestinationName() + c2.getReturnWhenLoadedDestTrackName());
161            case BY_FINAL_DEST:
162                return (c1, c2) -> (c1.getFinalDestinationName() + c1.getFinalDestinationTrackName())
163                        .compareToIgnoreCase(c2.getFinalDestinationName() + c2.getFinalDestinationTrackName());
164            case BY_ROUTE:
165                return (c1, c2) -> (c1.getRoutePath().compareToIgnoreCase(c2.getRoutePath()));
166            case BY_DIVISION:
167                return (c1, c2) -> (c1.getDivisionName().compareToIgnoreCase(c2.getDivisionName()));
168            case BY_WAIT:
169                return (c1, c2) -> (c1.getWait() - c2.getWait());
170            case BY_PICKUP:
171                return (c1, c2) -> (c1.getPickupScheduleName().compareToIgnoreCase(c2.getPickupScheduleName()));
172            case BY_HAZARD:
173                return (c1, c2) -> ((c1.isHazardous() ? 1 : 0) - (c2.isHazardous() ? 1 : 0));
174            case BY_SPLIT_FINAL_DEST:
175                return (c1, c2) -> (c1.getSplitFinalDestinationName() + c1.getSplitFinalDestinationTrackName())
176                        .compareToIgnoreCase(
177                                c2.getSplitFinalDestinationName() + c2.getSplitFinalDestinationTrackName());
178            case BY_SPLIT_LOCATION:
179                return (c1, c2) -> (c1.getStatus() + c1.getSplitLocationName() + c1.getSplitTrackName())
180                        .compareToIgnoreCase(c2.getStatus() + c2.getSplitLocationName() + c2.getSplitTrackName());
181            case BY_SPLIT_DESTINATION:
182                return (c1, c2) -> (c1.getSplitDestinationName() + c1.getSplitDestinationTrackName())
183                        .compareToIgnoreCase(c2.getSplitDestinationName() + c2.getSplitDestinationTrackName());
184            default:
185                return super.getComparator(attribute);
186        }
187    }
188
189    /**
190     * Return a list available cars (no assigned train or car already assigned
191     * to this train) on a route, cars are ordered least recently moved to most
192     * recently moved. Note that it is possible for a car to have a location,
193     * but no track assignment.
194     *
195     * @param train The Train to use.
196     * @return List of cars with no assigned train on a route
197     */
198    public List<Car> getAvailableTrainList(Train train) {
199        List<Car> out = new ArrayList<>();
200        Route route = train.getRoute();
201        if (route == null) {
202            return out;
203        }
204        // get a list of locations served by this route
205        List<RouteLocation> routeList = route.getLocationsBySequenceList();
206        // don't include Car at route destination
207        RouteLocation rlDestination = null;
208        if (routeList.size() > 1) {
209            rlDestination = routeList.get(routeList.size() - 1);
210            // However, if the destination is visited more than once, must
211            // include all cars
212            for (int i = 0; i < routeList.size() - 1; i++) {
213                if (rlDestination.getName().equals(routeList.get(i).getName())) {
214                    rlDestination = null; // include cars at destination
215                    break;
216                }
217            }
218            // local moves allowed at destination? Don't include cars in staging
219            if (rlDestination != null &&
220                    rlDestination.isLocalMovesAllowed() &&
221                    rlDestination.getLocation() != null &&
222                    !rlDestination.getLocation().isStaging()) {
223                rlDestination = null; // include cars at destination
224            }
225        }
226        // get rolling stock by track priority, load priority and then by moves
227        List<Car> sortByPriority = sortByTrackPriority(sortByLoadPriority(getByMovesList()));
228        // now build list of available cars for this route
229        for (Car car : sortByPriority) {
230            // only use cars with a location
231            if (car.getLocation() == null) {
232                continue;
233            }
234            RouteLocation rl = route.getLastLocationByName(car.getLocationName());
235            // only allow cars that don't have an assigned train, or the
236            // assigned train is this one
237            if (rl != null && rl != rlDestination && (car.getTrain() == null || train.equals(car.getTrain()))) {
238                out.add(car);
239            }
240        }
241        return out;
242    }
243
244    // sorts the high priority cars to the start of the list
245    protected List<Car> sortByLoadPriority(List<Car> list) {
246        List<Car> out = new ArrayList<>();
247        // move high priority cars to the start
248        for (Car car : list) {
249            if (car.getLoadPriority().equals(CarLoad.PRIORITY_HIGH)) {
250                out.add(car);
251            }
252        }
253        for (Car car : list) {
254            if (car.getLoadPriority().equals(CarLoad.PRIORITY_MEDIUM)) {
255                out.add(car);
256            }
257        }
258        // now load all of the remaining low priority cars
259        for (Car car : list) {
260            if (!out.contains(car)) {
261                out.add(car);
262            }
263        }
264        return out;
265    }
266
267    /**
268     * Provides a very sorted list of cars assigned to the train. Note that this
269     * isn't the final sort as the cars must be sorted by each location the
270     * train visits.
271     * <p>
272     * The sort priority is as follows:
273     * <ol>
274     * <li>Caboose or car with FRED to the end of the list, unless passenger.
275     * <li>Passenger cars have blocking numbers which places them relative to
276     * each other. Passenger cars with positive blocking numbers to the end of
277     * the list, but before cabooses or car with FRED. Passenger cars with
278     * negative blocking numbers are placed at the front of the train.
279     * <li>Car's destination (alphabetical by location and track name or by
280     * track blocking order)
281     * <li>Car is hazardous (hazardous placed after a non-hazardous car)
282     * <li>Car's current location (alphabetical by location and track name)
283     * <li>Car's final destination (alphabetical by location and track name)
284     * </ol>
285     * <p>
286     * Cars in a kernel are placed together by their kernel blocking numbers,
287     * except if they are type passenger. The kernel's position in the list is
288     * based on the lead car in the kernel.
289     * <p>
290     * If the train is to be blocked by track blocking order, all of the tracks
291     * at that location need a blocking number greater than 0.
292     *
293     * @param train The selected Train.
294     * @return Ordered list of cars assigned to the train
295     */
296    public List<Car> getByTrainDestinationList(Train train) {
297        List<Car> byLoadType = getByList(getList(train), BY_LOAD_TYPE);
298        List<Car> byFinal = getByList(byLoadType, BY_SPLIT_FINAL_DEST);
299        List<Car> byLocation = getByList(byFinal, BY_SPLIT_LOCATION);
300        List<Car> byHazard = getByList(byLocation, BY_HAZARD);
301        List<Car> byDestination = getByList(byHazard, BY_SPLIT_DESTINATION);
302        // now place cabooses, cars with FRED, and passenger cars at the rear of the
303        // train
304        List<Car> out = new ArrayList<>();
305        int lastCarsIndex = 0; // incremented each time a car is added to the end of the list
306        for (Car car : byDestination) {
307            if (car.getKernel() != null && !car.isLead() && !car.isPassenger()) {
308                continue; // not the lead car, skip for now.
309            }
310            if (!car.isCaboose() && !car.hasFred() && !car.isPassenger()) {
311                // sort order based on train direction when serving track, low to high if West
312                // or North bound trains
313                if (car.getDestinationTrack() != null && car.getDestinationTrack().getBlockingOrder() > 0) {
314                    for (int j = 0; j < out.size(); j++) {
315                        if (out.get(j).getDestinationTrack() == null) {
316                            continue;
317                        }
318                        if (car.getRouteDestination() != null &&
319                                (car.getRouteDestination().getTrainDirectionString().equals(RouteLocation.WEST_DIR) ||
320                                        car.getRouteDestination().getTrainDirectionString()
321                                                .equals(RouteLocation.NORTH_DIR))) {
322                            if (car.getDestinationTrack().getBlockingOrder() < out.get(j).getDestinationTrack()
323                                    .getBlockingOrder()) {
324                                out.add(j, car);
325                                break;
326                            }
327                            // Train is traveling East or South when setting out the car
328                        } else {
329                            if (car.getDestinationTrack().getBlockingOrder() > out.get(j).getDestinationTrack()
330                                    .getBlockingOrder()) {
331                                out.add(j, car);
332                                break;
333                            }
334                        }
335                    }
336                }
337                if (!out.contains(car)) {
338                    out.add(out.size() - lastCarsIndex, car);
339                }
340            } else if (car.isPassenger()) {
341                if (car.getBlocking() < 0) {
342                    // block passenger cars with negative blocking numbers at
343                    // front of train
344                    int index;
345                    for (index = 0; index < out.size(); index++) {
346                        Car carTest = out.get(index);
347                        if (!carTest.isPassenger() || carTest.getBlocking() > car.getBlocking()) {
348                            break;
349                        }
350                    }
351                    out.add(index, car);
352                } else {
353                    // block passenger cars at end of list, but before cabooses
354                    // or car with FRED
355                    int index;
356                    for (index = 0; index < lastCarsIndex; index++) {
357                        Car carTest = out.get(out.size() - 1 - index);
358                        log.debug("Car ({}) has blocking number: {}", carTest.toString(), carTest.getBlocking());
359                        if (carTest.isPassenger() &&
360                                !carTest.isCaboose() &&
361                                !carTest.hasFred() &&
362                                carTest.getBlocking() < car.getBlocking()) {
363                            break;
364                        }
365                    }
366                    out.add(out.size() - index, car);
367                    lastCarsIndex++;
368                }
369            } else if (car.isCaboose() || car.hasFred()) {
370                out.add(car); // place at end of list
371                lastCarsIndex++;
372            }
373            // group the cars in the kernel together, except passenger
374            if (car.isLead()) {
375                int index = out.indexOf(car);
376                int numberOfCars = 1; // already added the lead car to the list
377                for (Car kcar : car.getKernel().getCars()) {
378                    if (car != kcar && !kcar.isPassenger()) {
379                        // Block cars in kernel
380                        for (int j = 0; j < numberOfCars; j++) {
381                            if (kcar.getBlocking() < out.get(index + j).getBlocking()) {
382                                out.add(index + j, kcar);
383                                break;
384                            }
385                        }
386                        if (!out.contains(kcar)) {
387                            out.add(index + numberOfCars, kcar);
388                        }
389                        numberOfCars++;
390                        if (car.hasFred() || car.isCaboose() || car.isPassenger() && car.getBlocking() > 0) {
391                            lastCarsIndex++; // place entire kernel at the end of list
392                        }
393                    }
394                }
395            }
396        }
397        return out;
398    }
399
400    /**
401     * Get a list of car road names where the car was flagged as a caboose.
402     *
403     * @return List of caboose road names.
404     */
405    public List<String> getCabooseRoadNames() {
406        List<String> names = new ArrayList<>();
407        Enumeration<String> en = _hashTable.keys();
408        while (en.hasMoreElements()) {
409            Car car = getById(en.nextElement());
410            if (car.isCaboose() && !names.contains(car.getRoadName())) {
411                names.add(car.getRoadName());
412            }
413        }
414        java.util.Collections.sort(names);
415        return names;
416    }
417
418    /**
419     * Get a list of car road names where the car was flagged with FRED
420     *
421     * @return List of road names of cars with FREDs
422     */
423    public List<String> getFredRoadNames() {
424        List<String> names = new ArrayList<>();
425        Enumeration<String> en = _hashTable.keys();
426        while (en.hasMoreElements()) {
427            Car car = getById(en.nextElement());
428            if (car.hasFred() && !names.contains(car.getRoadName())) {
429                names.add(car.getRoadName());
430            }
431        }
432        java.util.Collections.sort(names);
433        return names;
434    }
435
436    /**
437     * Replace car loads
438     *
439     * @param type        type of car
440     * @param oldLoadName old load name
441     * @param newLoadName new load name
442     */
443    public void replaceLoad(String type, String oldLoadName, String newLoadName) {
444        List<Car> cars = getList();
445        for (Car car : cars) {
446            if (car.getTypeName().equals(type) && car.getLoadName().equals(oldLoadName)) {
447                if (newLoadName != null) {
448                    car.setLoadName(newLoadName);
449                } else {
450                    car.setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
451                }
452            }
453            if (car.getTypeName().equals(type) && car.getReturnWhenEmptyLoadName().equals(oldLoadName)) {
454                if (newLoadName != null) {
455                    car.setReturnWhenEmptyLoadName(newLoadName);
456                } else {
457                    car.setReturnWhenEmptyLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
458                }
459            }
460            if (car.getTypeName().equals(type) && car.getReturnWhenLoadedLoadName().equals(oldLoadName)) {
461                if (newLoadName != null) {
462                    car.setReturnWhenLoadedLoadName(newLoadName);
463                } else {
464                    car.setReturnWhenLoadedLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultLoadName());
465                }
466            }
467        }
468    }
469
470    public List<Car> getCarsLocationUnknown() {
471        List<Car> mias = new ArrayList<>();
472        for (Car car : getByIdList()) {
473            if (car.isLocationUnknown()) {
474                mias.add(car); // return unknown location car
475            }
476        }
477        return mias;
478    }
479
480    /**
481     * Determines a car's weight in ounces based on car's scale length
482     * 
483     * @param carLength Car's scale length
484     * @return car's weight in ounces
485     * @throws NumberFormatException if length isn't a number
486     */
487    public static String calculateCarWeight(String carLength) throws NumberFormatException {
488        double doubleCarLength = Double.parseDouble(carLength) * 12 / Setup.getScaleRatio();
489        double doubleCarWeight = (Setup.getInitalWeight() + doubleCarLength * Setup.getAddWeight()) / 1000;
490        NumberFormat nf = NumberFormat.getNumberInstance();
491        nf.setMaximumFractionDigits(1);
492        return nf.format(doubleCarWeight); // car weight in ounces.
493    }
494
495    /**
496     * Used to determine if any car has been assigned a division
497     * 
498     * @return true if any car has been assigned a division, otherwise false
499     */
500    public boolean isThereDivisions() {
501        for (Car car : getList()) {
502            if (car.getDivision() != null) {
503                return true;
504            }
505        }
506        return false;
507    }
508
509    /**
510     * Used to determine if there are clone cars.
511     * 
512     * @return true if there are clone cars, otherwise false.
513     */
514    public boolean isThereClones() {
515        for (Car car : getList()) {
516            if (car.isClone()) {
517                return true;
518            }
519        }
520        return false;
521    }
522
523    /**
524     * Creates a clone for the car, and clones if the car is part of a kernel.
525     * Note that a car have have multiple clones.
526     * 
527     * @param car       The car to clone
528     * @param track     The destination track for the clones
529     * @param train     The train transporting the clones
530     * @param startTime The date and time the clones were moved
531     * @return clone for this car
532     */
533    public Car createClone(Car car, Track track, Train train, LocalDateTime startTime) {
534        Car clone = createClone(car);
535        // for reset
536        clone.setPreviousFinalDestination(car.getPreviousFinalDestination());
537        clone.setPreviousFinalDestinationTrack(car.getPreviousFinalDestinationTrack());
538        clone.setPreviousScheduleId(car.getScheduleItemId());
539        createCloneKernel(car, track, train, startTime, clone);
540        // move car to new location for later pick up
541        finshCreateClone(car, track, train, startTime, clone);
542        return clone;
543    }
544
545    private void createCloneKernel(Car car, Track track, Train train, LocalDateTime startTime, Car cloneCar) {
546        if (car.getKernel() != null) {
547            String kernelName = car.getKernelName() + Car.CLONE + padNumber(car.getCloneOrder());
548            Kernel kernel = InstanceManager.getDefault(KernelManager.class).newKernel(kernelName);
549            cloneCar.setKernel(kernel);
550            for (Car kar : car.getKernel().getCars()) {
551                if (kar != car) {
552                    Car nClone = createClone(kar, car.getCloneOrder());
553                    nClone.setKernel(kernel);
554                    // for reset
555                    nClone.setPreviousFinalDestination(car.getPreviousFinalDestination());
556                    nClone.setPreviousFinalDestinationTrack(car.getPreviousFinalDestinationTrack());
557                    // move car to new location for later pick up
558                    finshCreateClone(kar, track, train, startTime, nClone);
559                }
560            }
561        }
562    }
563
564    int _commentLength = 0;
565
566    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
567            justification = "I18N of Info Message")
568    public int getMaxCommentLength() {
569        if (_commentLength == 0) {
570            _commentLength = TrainManifestHeaderText.getStringHeader_Comment().length();
571            String comment = "";
572            Car carMax = null;
573            for (Car car : getList()) {
574                if (car.getComment().length() > _commentLength) {
575                    _commentLength = car.getComment().length();
576                    comment = car.getComment();
577                    carMax = car;
578                }
579            }
580            if (carMax != null) {
581                log.info(Bundle.getMessage("InfoMaxComment", carMax.toString(), comment, _commentLength));
582            }
583        }
584        return _commentLength;
585    }
586
587    public void load(Element root) {
588        if (root.getChild(Xml.CARS) != null) {
589            List<Element> eCars = root.getChild(Xml.CARS).getChildren(Xml.CAR);
590            log.debug("readFile sees {} cars", eCars.size());
591            for (Element eCar : eCars) {
592                register(new Car(eCar));
593            }
594        }
595    }
596
597    /**
598     * Create an XML element to represent this Entry. This member has to remain
599     * synchronized with the detailed DTD in operations-cars.dtd.
600     *
601     * @param root The common Element for operations-cars.dtd.
602     */
603    public void store(Element root) {
604        // nothing to save under options
605        root.addContent(new Element(Xml.OPTIONS));
606
607        Element values;
608        root.addContent(values = new Element(Xml.CARS));
609        // add entries
610        List<Car> carList = getByIdList();
611        for (Car rs : carList) {
612            Car car = rs;
613            values.addContent(car.store());
614        }
615    }
616
617    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
618        // Set dirty
619        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
620        super.firePropertyChange(p, old, n);
621    }
622
623    @Override
624    public void propertyChange(PropertyChangeEvent evt) {
625        if (evt.getPropertyName().equals(Car.COMMENT_CHANGED_PROPERTY)) {
626            _commentLength = 0;
627        }
628        super.propertyChange(evt);
629    }
630
631    private static final Logger log = LoggerFactory.getLogger(CarManager.class);
632
633    @Override
634    public void initialize() {
635        InstanceManager.getDefault(OperationsSetupXml.class); // load setup
636        // create manager to load cars and their attributes
637        InstanceManager.getDefault(CarManagerXml.class);
638    }
639
640}