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