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