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