001package jmri.jmrit.operations.rollingstock.cars;
002
003import java.text.NumberFormat;
004import java.util.*;
005
006import org.jdom2.Element;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import jmri.*;
011import jmri.jmrit.operations.rollingstock.RollingStockManager;
012import jmri.jmrit.operations.routes.Route;
013import jmri.jmrit.operations.routes.RouteLocation;
014import jmri.jmrit.operations.setup.OperationsSetupXml;
015import jmri.jmrit.operations.setup.Setup;
016import jmri.jmrit.operations.trains.Train;
017
018/**
019 * Manages the cars.
020 *
021 * @author Daniel Boudreau Copyright (C) 2008
022 */
023public class CarManager extends RollingStockManager<Car>
024        implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize {
025
026    public CarManager() {
027    }
028
029    /**
030     * Finds an existing Car or creates a new Car if needed requires car's road and
031     * number
032     *
033     * @param road   car road
034     * @param number car number
035     * @return new car or existing Car
036     */
037    @Override
038    public Car newRS(String road, String number) {
039        Car car = getByRoadAndNumber(road, number);
040        if (car == null) {
041            car = new Car(road, number);
042            register(car);
043        }
044        return car;
045    }
046
047    @Override
048    public void deregister(Car car) {
049        super.deregister(car);
050        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
051    }
052
053    /**
054     * Sort by rolling stock location
055     *
056     * @return list of cars ordered by the Car's location
057     */
058    @Override
059    public List<Car> getByLocationList() {
060        return getByList(getByKernelList(), BY_LOCATION);
061    }
062
063    /**
064     * Sort by car kernel names
065     *
066     * @return list of cars ordered by car kernel
067     */
068    public List<Car> getByKernelList() {
069        return getByList(getByList(getByNumberList(), BY_BLOCKING), BY_KERNEL);
070    }
071
072    /**
073     * Sort by car loads
074     *
075     * @return list of cars ordered by car loads
076     */
077    public List<Car> getByLoadList() {
078        return getByList(getByLocationList(), BY_LOAD);
079    }
080
081    /**
082     * Sort by car return when empty location and track
083     *
084     * @return list of cars ordered by car return when empty
085     */
086    public List<Car> getByRweList() {
087        return getByList(getByLocationList(), BY_RWE);
088    }
089
090    public List<Car> getByRwlList() {
091        return getByList(getByLocationList(), BY_RWL);
092    }
093
094    public List<Car> getByDivisionList() {
095        return getByList(getByLocationList(), BY_DIVISION);
096    }
097
098    public List<Car> getByFinalDestinationList() {
099        return getByList(getByDestinationList(), BY_FINAL_DEST);
100    }
101
102    /**
103     * Sort by car wait count
104     *
105     * @return list of cars ordered by wait count
106     */
107    public List<Car> getByWaitList() {
108        return getByList(getByIdList(), BY_WAIT);
109    }
110
111    public List<Car> getByPickupList() {
112        return getByList(getByIdList(), BY_PICKUP);
113    }
114
115    // The special sort options for cars
116    private static final int BY_LOAD = 4;
117    private static final int BY_KERNEL = 5;
118    private static final int BY_RWE = 13; // Return When Empty
119    private static final int BY_FINAL_DEST = 14;
120    private static final int BY_WAIT = 16;
121    private static final int BY_PICKUP = 19;
122    private static final int BY_HAZARD = 21;
123    private static final int BY_RWL = 22; // Return When loaded
124    private static final int BY_DIVISION = 23;
125
126    // add car options to sort comparator
127    @Override
128    protected java.util.Comparator<Car> getComparator(int attribute) {
129        switch (attribute) {
130            case BY_LOAD:
131                return (c1, c2) -> (c1.getLoadName().compareToIgnoreCase(c2.getLoadName()));
132            case BY_KERNEL:
133                return (c1, c2) -> (c1.getKernelName().compareToIgnoreCase(c2.getKernelName()));
134            case BY_RWE:
135                return (c1,
136                        c2) -> (c1.getReturnWhenEmptyDestName().compareToIgnoreCase(c2.getReturnWhenEmptyDestName()));
137            case BY_RWL:
138                return (c1,
139                        c2) -> (c1.getReturnWhenLoadedDestName().compareToIgnoreCase(c2.getReturnWhenLoadedDestName()));
140            case BY_FINAL_DEST:
141                return (c1, c2) -> (c1.getFinalDestinationName().compareToIgnoreCase(c2.getFinalDestinationName()));
142            case BY_DIVISION:
143                return (c1, c2) -> (c1.getDivisionName().compareToIgnoreCase(c2.getDivisionName()));
144            case BY_WAIT:
145                return (c1, c2) -> (c1.getWait() - c2.getWait());
146            case BY_PICKUP:
147                return (c1, c2) -> (c1.getPickupScheduleName().compareToIgnoreCase(c2.getPickupScheduleName()));
148            case BY_HAZARD:
149                return (c1, c2) -> ((c1.isHazardous() ? 1 : 0) - (c2.isHazardous() ? 1 : 0));
150            default:
151                return super.getComparator(attribute);
152        }
153    }
154
155    /**
156     * Return a list available cars (no assigned train or car already assigned to
157     * this train) on a route, cars are ordered least recently moved to most
158     * recently moved.
159     *
160     * @param train The Train to use.
161     *
162     * @return List of cars with no assigned train on a route
163     */
164    public List<Car> getAvailableTrainList(Train train) {
165        List<Car> out = new ArrayList<>();
166        Route route = train.getRoute();
167        if (route == null) {
168            return out;
169        }
170        // get a list of locations served by this route
171        List<RouteLocation> routeList = route.getLocationsBySequenceList();
172        // don't include Car at route destination
173        RouteLocation destination = null;
174        if (routeList.size() > 1) {
175            destination = routeList.get(routeList.size() - 1);
176            // However, if the destination is visited more than once, must
177            // include all cars
178            for (int i = 0; i < routeList.size() - 1; i++) {
179                if (destination.getName().equals(routeList.get(i).getName())) {
180                    destination = null; // include cars at destination
181                    break;
182                }
183            }
184            // pickup allowed at destination? Don't include cars in staging
185            if (destination != null &&
186                    destination.isPickUpAllowed() &&
187                    destination.getLocation() != null &&
188                    !destination.getLocation().isStaging()) {
189                destination = null; // include cars at destination
190            }
191        }
192        // get rolling stock by priority and then by moves
193        List<Car> sortByPriority = sortByPriority(getByMovesList());
194        // now build list of available Car for this route
195        for (Car car : sortByPriority) {
196            // only use Car with a location
197            if (car.getLocation() == null) {
198                continue;
199            }
200            RouteLocation rl = route.getLastLocationByName(car.getLocationName());
201            // get Car that don't have an assigned train, or the
202            // assigned train is this one
203            if (rl != null && rl != destination && (car.getTrain() == null || train.equals(car.getTrain()))) {
204                out.add(car);
205            }
206        }
207        return out;
208    }
209
210    // sorts the high priority cars to the start of the list
211    protected List<Car> sortByPriority(List<Car> list) {
212        List<Car> out = new ArrayList<>();
213        // move high priority cars to the start
214        for (Car car : list) {
215            if (car.getLoadPriority().equals(CarLoad.PRIORITY_HIGH)) {
216                out.add(car);
217            }
218        }
219        for (Car car : list) {
220            if (car.getLoadPriority().equals(CarLoad.PRIORITY_MEDIUM)) {
221                out.add(car);
222            }
223        }
224        // now load all of the remaining low priority cars
225        for (Car car : list) {
226            if (!out.contains(car)) {
227                out.add(car);
228            }
229        }
230        return out;
231    }
232
233    /**
234     * Provides a very sorted list of cars assigned to the train. Note that this
235     * isn't the final sort as the cars must be sorted by each location the
236     * train visits.
237     * <p>
238     * The sort priority is as follows:
239     * <ol>
240     * <li>Caboose or car with FRED to the end of the list, unless passenger.
241     * <li>Passenger cars have blocking numbers which places them relative to
242     * each other. Passenger cars with positive blocking numbers to the end of
243     * the list, but before cabooses or car with FRED. Passenger cars with
244     * negative blocking numbers are placed at the front of the train.
245     * <li>Car's destination (alphabetical by location and track name or by
246     * track blocking order)
247     * <li>Car is hazardous (hazardous placed after a non-hazardous car)
248     * <li>Car's current location (alphabetical by location and track name)
249     * <li>Car's final destination (alphabetical by location and track name)
250     * </ol>
251     * <p>
252     * Cars in a kernel are placed together by their kernel blocking numbers,
253     * except if they are type passenger. The kernel's position in the list is
254     * based on the lead car in the kernel.
255     * <p>
256     * If the train is to be blocked by track blocking order, all of the tracks
257     * at that location need a blocking number greater than 0.
258     *
259     * @param train The selected Train.
260     * @return Ordered list of cars assigned to the train
261     */
262    public List<Car> getByTrainDestinationList(Train train) {
263        List<Car> byFinal = getByList(getList(train), BY_FINAL_DEST);
264        List<Car> byLocation = getByList(byFinal, BY_LOCATION);
265        List<Car> byHazard = getByList(byLocation, BY_HAZARD);
266        List<Car> byDestination = getByList(byHazard, BY_DESTINATION);
267        // now place cabooses, cars with FRED, and passenger cars at the rear of the
268        // train
269        List<Car> out = new ArrayList<>();
270        int lastCarsIndex = 0; // incremented each time a car is added to the end of the list
271        for (Car car : byDestination) {
272            if (car.getKernel() != null && !car.isLead() && !car.isPassenger()) {
273                continue; // not the lead car, skip for now.
274            }
275            if (!car.isCaboose() && !car.hasFred() && !car.isPassenger()) {
276                // sort order based on train direction when serving track, low to high if West
277                // or North bound trains
278                if (car.getDestinationTrack() != null && car.getDestinationTrack().getBlockingOrder() > 0) {
279                    for (int j = 0; j < out.size(); j++) {
280                        if (out.get(j).getDestinationTrack() == null) {
281                            continue;
282                        }
283                        if (car.getRouteDestination() != null &&
284                                (car.getRouteDestination().getTrainDirectionString().equals(RouteLocation.WEST_DIR) ||
285                                        car.getRouteDestination().getTrainDirectionString()
286                                                .equals(RouteLocation.NORTH_DIR))) {
287                            if (car.getDestinationTrack().getBlockingOrder() < out.get(j).getDestinationTrack()
288                                    .getBlockingOrder()) {
289                                out.add(j, car);
290                                break;
291                            }
292                            // Train is traveling East or South when setting out the car
293                        } else {
294                            if (car.getDestinationTrack().getBlockingOrder() > out.get(j).getDestinationTrack()
295                                    .getBlockingOrder()) {
296                                out.add(j, car);
297                                break;
298                            }
299                        }
300                    }
301                }
302                if (!out.contains(car)) {
303                    out.add(out.size() - lastCarsIndex, car);
304                }
305            } else if (car.isPassenger()) {
306                if (car.getBlocking() < 0) {
307                    // block passenger cars with negative blocking numbers at
308                    // front of train
309                    int index;
310                    for (index = 0; index < out.size(); index++) {
311                        Car carTest = out.get(index);
312                        if (!carTest.isPassenger() || carTest.getBlocking() > car.getBlocking()) {
313                            break;
314                        }
315                    }
316                    out.add(index, car);
317                } else {
318                    // block passenger cars at end of list, but before cabooses
319                    // or car with FRED
320                    int index;
321                    for (index = 0; index < lastCarsIndex; index++) {
322                        Car carTest = out.get(out.size() - 1 - index);
323                        log.debug("Car ({}) has blocking number: {}", carTest.toString(), carTest.getBlocking());
324                        if (carTest.isPassenger() &&
325                                !carTest.isCaboose() &&
326                                !carTest.hasFred() &&
327                                carTest.getBlocking() < car.getBlocking()) {
328                            break;
329                        }
330                    }
331                    out.add(out.size() - index, car);
332                    lastCarsIndex++;
333                }
334            } else if (car.isCaboose() || car.hasFred()) {
335                out.add(car); // place at end of list
336                lastCarsIndex++;
337            }
338            // group the cars in the kernel together, except passenger
339            if (car.isLead()) {
340                int index = out.indexOf(car);
341                int numberOfCars = 1; // already added the lead car to the list
342                for (Car kcar : car.getKernel().getCars()) {
343                    if (car != kcar && !kcar.isPassenger()) {
344                        // Block cars in kernel
345                        for (int j = 0; j < numberOfCars; j++) {
346                            if (kcar.getBlocking() < out.get(index + j).getBlocking()) {
347                                out.add(index + j, kcar);
348                                break;
349                            }
350                        }
351                        if (!out.contains(kcar)) {
352                            out.add(index + numberOfCars, kcar);
353                        }
354                        numberOfCars++;
355                        if (car.hasFred() || car.isCaboose() || car.isPassenger() && car.getBlocking() > 0) {
356                            lastCarsIndex++; // place entire kernel at the end of list
357                        }
358                    }
359                }
360            }
361        }
362        return out;
363    }
364
365    /**
366     * Get a list of car road names where the car was flagged as a caboose.
367     *
368     * @return List of caboose road names.
369     */
370    public List<String> getCabooseRoadNames() {
371        List<String> names = new ArrayList<>();
372        Enumeration<String> en = _hashTable.keys();
373        while (en.hasMoreElements()) {
374            Car car = getById(en.nextElement());
375            if (car.isCaboose() && !names.contains(car.getRoadName())) {
376                names.add(car.getRoadName());
377            }
378        }
379        java.util.Collections.sort(names);
380        return names;
381    }
382
383    /**
384     * Get a list of car road names where the car was flagged with FRED
385     *
386     * @return List of road names of cars with FREDs
387     */
388    public List<String> getFredRoadNames() {
389        List<String> names = new ArrayList<>();
390        Enumeration<String> en = _hashTable.keys();
391        while (en.hasMoreElements()) {
392            Car car = getById(en.nextElement());
393            if (car.hasFred() && !names.contains(car.getRoadName())) {
394                names.add(car.getRoadName());
395            }
396        }
397        java.util.Collections.sort(names);
398        return names;
399    }
400
401    /**
402     * Replace car loads
403     *
404     * @param type        type of car
405     * @param oldLoadName old load name
406     * @param newLoadName new load name
407     */
408    public void replaceLoad(String type, String oldLoadName, String newLoadName) {
409        List<Car> cars = getList();
410        for (Car car : cars) {
411            if (car.getTypeName().equals(type) && car.getLoadName().equals(oldLoadName)) {
412                if (newLoadName != null) {
413                    car.setLoadName(newLoadName);
414                } else {
415                    car.setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
416                }
417            }
418            if (car.getTypeName().equals(type) && car.getReturnWhenEmptyLoadName().equals(oldLoadName)) {
419                if (newLoadName != null) {
420                    car.setReturnWhenEmptyLoadName(newLoadName);
421                } else {
422                    car.setReturnWhenEmptyLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName());
423                }
424            }
425            if (car.getTypeName().equals(type) && car.getReturnWhenLoadedLoadName().equals(oldLoadName)) {
426                if (newLoadName != null) {
427                    car.setReturnWhenLoadedLoadName(newLoadName);
428                } else {
429                    car.setReturnWhenLoadedLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultLoadName());
430                }
431            }
432        }
433    }
434
435    public List<Car> getCarsLocationUnknown() {
436        List<Car> mias = new ArrayList<>();
437        List<Car> cars = getByIdList();
438        for (Car rs : cars) {
439            Car car = rs;
440            if (car.isLocationUnknown()) {
441                mias.add(car); // return unknown location car
442            }
443        }
444        return mias;
445    }
446
447    /**
448     * Determines a car's weight in ounces based on car's scale length
449     * 
450     * @param carLength Car's scale length
451     * @return car's weight in ounces
452     * @throws NumberFormatException if length isn't a number
453     */
454    public static String calculateCarWeight(String carLength) throws NumberFormatException {
455        double doubleCarLength = Double.parseDouble(carLength) * 12 / Setup.getScaleRatio();
456        double doubleCarWeight = (Setup.getInitalWeight() + doubleCarLength * Setup.getAddWeight()) / 1000;
457        NumberFormat nf = NumberFormat.getNumberInstance();
458        nf.setMaximumFractionDigits(1);
459        return nf.format(doubleCarWeight); // car weight in ounces.
460    }
461
462    public void load(Element root) {
463        if (root.getChild(Xml.CARS) != null) {
464            List<Element> eCars = root.getChild(Xml.CARS).getChildren(Xml.CAR);
465            log.debug("readFile sees {} cars", eCars.size());
466            for (Element eCar : eCars) {
467                register(new Car(eCar));
468            }
469        }
470    }
471
472    /**
473     * Create an XML element to represent this Entry. This member has to remain
474     * synchronized with the detailed DTD in operations-cars.dtd.
475     *
476     * @param root The common Element for operations-cars.dtd.
477     */
478    public void store(Element root) {
479        // nothing to save under options
480        root.addContent(new Element(Xml.OPTIONS));
481        
482        Element values;
483        root.addContent(values = new Element(Xml.CARS));
484        // add entries
485        List<Car> carList = getByIdList();
486        for (Car rs : carList) {
487            Car car = rs;
488            values.addContent(car.store());
489        }
490    }
491
492    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
493        // Set dirty
494        InstanceManager.getDefault(CarManagerXml.class).setDirty(true);
495        super.firePropertyChange(p, old, n);
496    }
497
498    private final static Logger log = LoggerFactory.getLogger(CarManager.class);
499
500    @Override
501    public void initialize() {
502        InstanceManager.getDefault(OperationsSetupXml.class); // load setup
503        // create manager to load cars and their attributes
504        InstanceManager.getDefault(CarManagerXml.class);
505    }
506
507}