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