001package jmri.jmrit.operations.router;
002
003import java.io.PrintWriter;
004import java.text.MessageFormat;
005import java.time.LocalDateTime;
006import java.time.ZoneId;
007import java.util.*;
008
009import jmri.InstanceManager;
010import jmri.InstanceManagerAutoDefault;
011import jmri.jmrit.operations.locations.Location;
012import jmri.jmrit.operations.locations.Track;
013import jmri.jmrit.operations.rollingstock.RollingStock;
014import jmri.jmrit.operations.rollingstock.cars.Car;
015import jmri.jmrit.operations.setup.Setup;
016import jmri.jmrit.operations.trains.Train;
017import jmri.jmrit.operations.trains.TrainManager;
018import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
019
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023/**
024 * Router for car movement. This code attempts to find a way (a route) to move a
025 * car to its final destination through the use of two or more trains. First the
026 * code tries to move car using a single train. If that fails, attempts are made
027 * using two trains via a classification/interchange (C/I) tracks, then yard
028 * tracks if enabled. Next attempts are made using three or more trains using
029 * any combination of C/I and yard tracks. If that fails and routing via staging
030 * is enabled, the code tries two trains using staging tracks, then multiple
031 * trains using a combination of C/I, yards, and staging tracks. Currently the
032 * router is limited to seven trains.
033 *
034 * @author Daniel Boudreau Copyright (C) 2010, 2011, 2012, 2013, 2015, 2021,
035 *         2022, 2024, 2026
036 */
037public class Router extends TrainCommon implements InstanceManagerAutoDefault {
038
039    TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
040
041    protected final List<Track> _nextLocationTracks = new ArrayList<>();
042    protected final List<Track> _lastLocationTracks = new ArrayList<>();
043    private final List<Track> _otherLocationTracks = new ArrayList<>();
044
045    protected final List<Track> _next2ndLocationTracks = new ArrayList<>();
046    protected final List<Track> _next3rdLocationTracks = new ArrayList<>();
047    protected final List<Track> _next4thLocationTracks = new ArrayList<>();
048
049    protected final List<Train> _nextLocationTrains = new ArrayList<>();
050    protected final List<Train> _lastLocationTrains = new ArrayList<>();
051    protected List<Train> _excludeTrains;
052
053    protected Hashtable<String, Train> _listTrains = new Hashtable<>();
054
055    protected static final String STATUS_NOT_THIS_TRAIN = Bundle.getMessage("RouterTrain");
056    public static final String STATUS_NOT_THIS_TRAIN_PREFIX =
057            STATUS_NOT_THIS_TRAIN.substring(0, STATUS_NOT_THIS_TRAIN.indexOf('('));
058    protected static final String STATUS_NOT_ABLE = Bundle.getMessage("RouterNotAble");
059    protected static final String STATUS_ROUTER_DISABLED = Bundle.getMessage("RouterDisabled");
060
061    private String _status = "";
062    private Train _train = null;
063    PrintWriter _buildReport = null; // build report
064    LocalDateTime _startTime; // when routing started
065
066    private static final String SEVEN = Setup.BUILD_REPORT_VERY_DETAILED;
067    private boolean _addtoReport = false;
068    private boolean _addtoReportVeryDetailed = false;
069
070    /**
071     * Returns the status of the router when using the setDestination() for a
072     * car.
073     *
074     * @return Track.OKAY, STATUS_NOT_THIS_TRAIN, STATUS_NOT_ABLE,
075     *         STATUS_ROUTER_DISABLED, or the destination track status is
076     *         there's an issue.
077     */
078    public String getStatus() {
079        return _status;
080    }
081
082    /**
083     * Determines if car can be routed to the destination track
084     * 
085     * @param car         the car being tested
086     * @param train       the first train servicing the car, can be null
087     * @param track       the destination track, can not be null
088     * @param buildReport the report, can be null
089     * @return true if the car can be routed to the track
090     */
091    public boolean isCarRouteable(Car car, Train train, Track track, PrintWriter buildReport) {
092        addLine(buildReport, SEVEN, Bundle.getMessage("RouterIsCarRoutable",
093                car.toString(), car.getLocationName(), car.getTrackName(), car.getLoadName(),
094                track.getLocation().getName(), track.getName()));
095        return isCarRouteable(car, train, track.getLocation(), track, buildReport);
096    }
097
098    public boolean isCarRouteable(Car car, Train train, Location destination, Track track, PrintWriter buildReport) {
099        Car c = car.copy();
100        c.setTrack(car.getTrack());
101        c.setFinalDestination(destination);
102        c.setFinalDestinationTrack(track);
103        c.setScheduleItemId(car.getScheduleItemId());
104        c.setRouteDestinationTiming(car.getRouteDestinationTiming());
105        boolean results = setDestination(c, train, buildReport);
106        c.setDestination(null, null); // clear router car destinations
107        c.setFinalDestinationTrack(null);
108        // transfer route path info
109        car.setRoutePath(c.getRoutePath());
110        return results;
111    }
112
113    /**
114     * Attempts to set the car's destination if a final destination exists. Only
115     * sets the car's destination if the train is part of the car's route.
116     *
117     * @param car         the car to route
118     * @param train       the first train to carry this car, can be null
119     * @param buildReport PrintWriter for build report, and can be null
120     * @return true if car can be routed.
121     */
122    public boolean setDestination(Car car, Train train, PrintWriter buildReport) {
123        if (car.getTrack() == null || car.getFinalDestination() == null) {
124            return false;
125        }
126        _startTime = LocalDateTime.now();
127        _status = Track.OKAY;
128        _train = train;
129        _buildReport = buildReport;
130        _addtoReport = Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_DETAILED) ||
131                Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_VERY_DETAILED);
132        _addtoReportVeryDetailed = Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_VERY_DETAILED);
133        log.debug("Car ({}) at location ({}, {}) final destination ({}, {}) car routing begins", car,
134                car.getLocationName(), car.getTrackName(), car.getFinalDestinationName(),
135                car.getFinalDestinationTrackName());
136        if (_train != null) {
137            log.debug("Routing using train ({})", train.getName());
138        }
139        // is car part of kernel?
140        if (car.getKernel() != null && !car.isLead()) {
141            return false;
142        }
143        // note clone car has the car's "final destination" as its destination
144        Car clone = clone(car);
145        // Note the following test doesn't check for car length which is what we
146        // want.
147        // Also ignores spur schedule since the car's destination is already
148        // set.
149        _status = clone.checkDestination(clone.getDestination(), clone.getDestinationTrack());
150        if (!_status.equals(Track.OKAY)) {
151            addLine(Bundle.getMessage("RouterCanNotDeliverCar",
152                    car.toString(), car.getFinalDestinationName(), car.getFinalDestinationTrackName(),
153                    _status, (car.getFinalDestinationTrack() == null ? Bundle.getMessage("RouterDestination")
154                            : car.getFinalDestinationTrack().getTrackTypeName())));
155            return false;
156        }
157        // check to see if car has a destination track or one is available
158        if (!checkForDestinationTrack(clone)) {
159            return false; // no destination track found
160        }
161        // check to see if car will move to destination using a single train
162        if (checkForSingleTrain(car, clone)) {
163            return true; // a single train can service this car
164        }
165        if (!Setup.isCarRoutingEnabled()) {
166            log.debug("Car ({}) final destination ({}) is not served directly by any train", car,
167                    car.getFinalDestinationName()); // NOI18N
168            _status = STATUS_ROUTER_DISABLED;
169            car.setFinalDestination(null);
170            car.setFinalDestinationTrack(null);
171            return false;
172        }
173        log.debug("Car ({}) final destination ({}) is not served by a single train", car,
174                car.getFinalDestinationName());
175        // was the request for a local move? Try multiple trains to move car
176        if (car.getLocationName().equals(car.getFinalDestinationName())) {
177            addLine(Bundle.getMessage("RouterCouldNotFindTrain",
178                    car.getLocationName(), car.getTrackName(), car.getFinalDestinationName(),
179                    car.getFinalDestinationTrackName()));
180        }
181        if (_addtoReport) {
182            addLine(Bundle.getMessage("RouterBeginTwoTrain",
183                    car.toString(), car.getLocationName(), car.getFinalDestinationName()));
184        }
185
186        setupLists();
187        excludeTrains(car);
188        excludeTracks();
189
190        // first try using 2 trains and an interchange track to route the car
191        if (setCarDestinationTwoTrainsInterchange(car)) {
192            if (car.getDestination() == null) {
193                log.debug(
194                        "Was able to find a route via classification/interchange track, but not using specified train" +
195                                " or car destination not set, try again using yard tracks"); // NOI18N
196                if (setCarDestinationTwoTrainsYard(car)) {
197                    log.debug("Was able to find route via yard ({}, {}) for car ({})", car.getDestinationName(),
198                            car.getDestinationTrackName(), car);
199                }
200            } else {
201                log.debug("Was able to find route via interchange ({}, {}) for car ({})", car.getDestinationName(),
202                        car.getDestinationTrackName(), car);
203            }
204            if (_addtoReportVeryDetailed) {
205                addLine(Bundle.getMessage("RouterTwoTrainsSuccess", car.toString()));
206            }
207            // now try 2 trains using a yard track
208        } else if (setCarDestinationTwoTrainsYard(car)) {
209            log.debug("Was able to find route via yard ({}, {}) for car ({}) using two trains",
210                    car.getDestinationName(), car.getDestinationTrackName(), car);
211            if (_addtoReportVeryDetailed) {
212                addLine(Bundle.getMessage("RouterTwoTrainsSuccess", car.toString()));
213            }
214            // now try 3 or more trains to route car, but not through staging
215        } else if (setCarDestinationMultipleTrains(car, false)) {
216            log.debug("Was able to find multiple train route for car ({})", car);
217            // now try 2 trains using a staging track to connect
218        } else if (setCarDestinationTwoTrainsStaging(car)) {
219            log.debug("Was able to find route via staging ({}, {}) for car ({}) using two trains",
220                    car.getDestinationName(), car.getDestinationTrackName(), car);
221            // now try 3 or more trains to route car, include staging if enabled
222        } else if (setCarDestinationMultipleTrains(car, true)) {
223            log.debug("Was able to find multiple train route for car ({}) through staging", car);
224        } else {
225            log.debug("Wasn't able to set route for car ({}) took {} mSec", car,
226                    LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() -
227                            _startTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
228            _status = STATUS_NOT_ABLE;
229            return false; // maybe next time
230        }
231        return true; // car's destination has been set
232    }
233
234    /*
235     * Checks to see if the car has a destination track, no destination track,
236     * searches for one. returns true if the car has a destination track or if
237     * there's one available.
238     */
239    private boolean checkForDestinationTrack(Car clone) {
240        if (clone.getDestination() != null && clone.getDestinationTrack() == null) {
241            // determine if there's a track that can service the car
242            String status = "";
243            for (Track track : clone.getDestination().getTracksList()) {
244                status = track.isRollingStockAccepted(clone);
245                if (status.equals(Track.OKAY) || status.startsWith(Track.LENGTH)) {
246                    log.debug("Track ({}) will accept car ({})", track.getName(), clone.toString());
247                    break;
248                }
249            }
250            if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
251                addLine(_status = Bundle.getMessage("RouterNoTracks",
252                        clone.getDestinationName(), clone.toString()));
253                return false;
254            }
255        }
256        return true;
257    }
258
259    /**
260     * Checks to see if a single train can transport car to its final
261     * destination. Special case if car is departing staging.
262     *
263     * @return true if single train can transport car to its final destination.
264     */
265    private boolean checkForSingleTrain(Car car, Car clone) {
266        boolean trainServicesCar = false; // true the specified train can service the car
267        Train testTrain = null;
268        if (_train != null) {
269            trainServicesCar = _train.isServiceable(_buildReport, clone);
270        }
271        if (trainServicesCar) {
272            testTrain = _train; // use the specified train
273            log.debug("Train ({}) can service car ({})", _train.getName(), car.toString());
274        } else if (_train != null && !_train.getServiceStatus().equals(Train.NONE)) {
275            // _train isn't able to service car
276            // determine if car was attempting to go to the train's termination staging
277            String trackName = car.getFinalDestinationTrackName();
278            if (car.getFinalDestinationTrack() == null &&
279                    car.getFinalDestinationName().equals(_train.getTrainTerminatesName()) &&
280                    _train.getTerminationTrack() != null) {
281                trackName = _train.getTerminationTrack().getName(); // use staging track
282            }
283            // report that train can't service car
284            addLine(Bundle.getMessage("RouterTrainCanNotDueTo", _train.getName(), car.toString(),
285                    car.getFinalDestinationName(), trackName, _train.getServiceStatus()));
286            if (!car.getTrack().isStaging() &&
287                    !_train.isServiceAllCarsWithFinalDestinationsEnabled()) {
288                _status = MessageFormat.format(STATUS_NOT_THIS_TRAIN, new Object[]{_train.getName()});
289                return true; // temporary issue with train moves, length, or destination track length
290            }
291        }
292        // Determines if specified train can service car out of staging.
293        // Note that the router code will try to route the car using
294        // two or more trains just to get the car out of staging.
295        if (car.getTrack().isStaging() && _train != null && !trainServicesCar) {
296            addLine(Bundle.getMessage("RouterTrainCanNotStaging",
297                    _train.getName(), car.toString(), car.getLocationName(),
298                    clone.getDestinationName(), clone.getDestinationTrackName()));
299            if (!_train.getServiceStatus().equals(Train.NONE)) {
300                addLine(_train.getServiceStatus());
301            }
302            addLine(Bundle.getMessage("RouterStagingTryRouting", car.toString(), clone.getLocationName(),
303                    clone.getDestinationName(), clone.getDestinationTrackName()));
304            // note that testTrain = null, return false
305        } else if (!trainServicesCar) {
306            List<Train> excludeTrains = new ArrayList<>(Arrays.asList(_train));
307            testTrain = trainManager.getTrainForCar(clone, excludeTrains, _buildReport, true);
308        }
309        // report that another train could transport the car
310        if (testTrain != null &&
311                _train != null &&
312                !trainServicesCar &&
313                _train.isServiceAllCarsWithFinalDestinationsEnabled()) {
314            // log.debug("Option to service all cars with a final destination is enabled");
315            addLine(Bundle.getMessage("RouterOptionToCarry",
316                    _train.getName(), testTrain.getName(), car.toString(),
317                    clone.getDestinationName(), clone.getDestinationTrackName()));
318            testTrain = null; // return false
319        }
320        if (testTrain != null) {
321            return finishRouteUsingOneTrain(testTrain, car, clone);
322        }
323        return false;
324    }
325
326    /**
327     * A single train can service the car. Provide various messages to build
328     * report detailing which train can service the car. Also checks to see if
329     * the needs to go the alternate track or yard track if the car's final
330     * destination track is full. Returns false if car is stuck in staging. Sets
331     * the car's destination if specified _train is available
332     *
333     * @return true for all cases except if car is departing staging and is
334     *         stuck there.
335     */
336    private boolean finishRouteUsingOneTrain(Train testTrain, Car car, Car clone) {
337        addLine(Bundle.getMessage("RouterTrainCanTransport", testTrain.getName(), car.toString(),
338                car.getTrack().getTrackTypeName(), car.getLocationName(), car.getTrackName(),
339                clone.getDestinationName(), clone.getDestinationTrackName()));
340        showRoute(car, new ArrayList<>(Arrays.asList(testTrain)),
341                new ArrayList<>(Arrays.asList(car.getFinalDestinationTrack())));
342        // don't modify car if a train wasn't specified
343        if (_train == null) {
344            return true; // done, car can be routed
345        }
346        // now check to see if specified train can service car directly
347        else if (_train != testTrain) {
348            addLine(Bundle.getMessage("TrainDoesNotServiceCar", _train.getName(), car.toString(),
349                    clone.getDestinationName(), clone.getDestinationTrackName()));
350            _status = MessageFormat.format(STATUS_NOT_THIS_TRAIN, new Object[]{testTrain.getName()});
351            return true; // car can be routed, but not by this train!
352        }
353        // adjust car timing for the car.setDestination
354        if (car.getRouteDestinationTiming() == null || car.getRouteDestinationTiming().getSequenceNumber() < clone.getRouteDestinationTiming().getSequenceNumber()) {
355            car.setRouteDestinationTiming(clone.getRouteDestinationTiming());
356        }
357        _status = car.setDestination(clone.getDestination(), clone.getDestinationTrack());
358        if (_status.equals(Track.OKAY)) {
359            return true; // done, car has new destination
360        }
361        addLine(Bundle.getMessage("RouterCanNotDeliverCar", car.toString(), clone.getDestinationName(),
362                clone.getDestinationTrackName(), _status,
363                (clone.getDestinationTrack() == null ? Bundle.getMessage("RouterDestination")
364                        : clone.getDestinationTrack().getTrackTypeName())));
365        // check to see if an alternative track was specified
366        if ((_status.startsWith(Track.LENGTH) || _status.startsWith(Track.SCHEDULE)) &&
367                clone.getDestinationTrack() != null &&
368                clone.getDestinationTrack().getAlternateTrack() != null &&
369                clone.getDestinationTrack().getAlternateTrack() != car.getTrack()) {
370            String status = car.setDestination(clone.getDestination(), clone.getDestinationTrack().getAlternateTrack());
371            if (status.equals(Track.OKAY)) {
372                if (_train.isServiceable(car)) {
373                    addLine(Bundle.getMessage("RouterSendCarToAlternative",
374                            car.toString(), clone.getDestinationTrack().getAlternateTrack().getName(),
375                            clone.getDestination().getName()));
376                    return true; // car is going to alternate track
377                }
378                addLine(Bundle.getMessage("RouterNotSendCarToAlternative", _train.getName(), car.toString(),
379                        clone.getDestinationTrack().getAlternateTrack().getName(),
380                        clone.getDestination().getName()));
381            } else {
382                addLine(Bundle.getMessage("RouterAlternateFailed",
383                        clone.getDestinationTrack().getAlternateTrack().getName(), status));
384            }
385        } else if (clone.getDestinationTrack() != null &&
386                clone.getDestinationTrack().getAlternateTrack() != null &&
387                clone.getDestinationTrack().getAlternateTrack() == car.getTrack()) {
388            // state that car is spotted at the alternative track
389            addLine(Bundle.getMessage("RouterAtAlternate",
390                    car.toString(), clone.getDestinationTrack().getAlternateTrack().getName(),
391                    clone.getLocationName(), clone.getDestinationTrackName()));
392        } else if (car.getLocation() == clone.getDestination()) {
393            // state that alternative and yard track options are not available
394            // if car is at final destination
395            addLine(Bundle.getMessage("RouterIgnoreAlternate", car.toString(), car.getLocationName()));
396        }
397        // check to see if spur was full, if so, forward to yard if possible
398        if (Setup.isForwardToYardEnabled() &&
399                _status.startsWith(Track.LENGTH) &&
400                car.getLocation() != clone.getDestination()) {
401            addLine(Bundle.getMessage("RouterSpurFull",
402                    clone.getDestinationName(), clone.getDestinationTrackName(), clone.getDestinationName()));
403            Location dest = clone.getDestination();
404            List<Track> yards = dest.getTracksByMoves(Track.YARD);
405            log.debug("Found {} yard(s) at destination ({})", yards.size(), clone.getDestinationName());
406            for (Track track : yards) {
407                String status = car.setDestination(dest, track);
408                if (status.equals(Track.OKAY)) {
409                    if (!_train.isServiceable(car)) {
410                        log.debug("Train ({}) can not deliver car ({}) to yard ({})", _train.getName(), car,
411                                track.getName());
412                        continue;
413                    }
414                    addLine(Bundle.getMessage("RouterSendCarToYard", car.toString(), dest.getName(), track.getName()));
415                    return true; // car is going to a yard
416                } else {
417                    addLine(Bundle.getMessage("RouterCanNotUseYard", track.getLocation().getName(), track.getName(),
418                            status));
419                }
420            }
421            addLine(Bundle.getMessage("RouterNoYardTracks", dest.getName(), car.toString()));
422        }
423        car.setDestination(null, null);
424        if (car.getTrack().isStaging()) {
425            addLine(Bundle.getMessage("RouterStagingTryRouting", car.toString(), clone.getLocationName(),
426                    clone.getDestinationName(), clone.getDestinationTrackName()));
427            return false; // try 2 or more trains
428        }
429        return true; // able to route, but unable to set the car's destination
430    }
431
432    private void setupLists() {
433        _nextLocationTracks.clear();
434        _next2ndLocationTracks.clear();
435        _next3rdLocationTracks.clear();
436        _next4thLocationTracks.clear();
437        _lastLocationTracks.clear();
438        _otherLocationTracks.clear();
439        _nextLocationTrains.clear();
440        _lastLocationTrains.clear();
441        _listTrains.clear();
442    }
443
444    private void excludeTrains(Car car) {
445        if (_addtoReportVeryDetailed) {
446            addLine(BLANK_LINE);
447            addLine(Bundle.getMessage("RouterExcludeTrains", car.toString(),
448                    car.getTypeName(), car.getLoadType().toLowerCase(), car.getLoadName(), car.getRoadName(),
449                    car.getBuilt(), car.getOwnerName()));
450        }
451        _excludeTrains = trainManager.getExcludeTrainListForCar(car, _buildReport);
452    }
453
454    /*
455     * No routing through alternate tracks. List them.
456     */
457    private void excludeTracks() {
458        if (_addtoReportVeryDetailed) {
459            addLine(BLANK_LINE);
460            addLine(Bundle.getMessage("RouterExcludeAltTracks"));
461            List<Track> tracks = locationManager.getTracks(null);
462            for (Track track : tracks) {
463                if (track.isAlternate() &&
464                        (track.getTrackType().equals(Track.INTERCHANGE) ||
465                                track.getTrackType().equals(Track.YARD) && Setup.isCarRoutingViaYardsEnabled())) {
466                    addLine(Bundle.getMessage("RouterExcludeAltTrack", track.getTrackTypeName(),
467                            track.getLocation().getName(), track.getName()));
468                }
469            }
470        }
471    }
472
473    /**
474     * Sets a car's destination to an interchange track if two trains can route
475     * the car.
476     *
477     * @param car the car to be routed
478     * @return true if car's destination has been modified to an interchange.
479     *         False if an interchange track wasn't found that could service the
480     *         car's final destination.
481     */
482    private boolean setCarDestinationTwoTrainsInterchange(Car car) {
483        return setCarDestinationTwoTrains(car, Track.INTERCHANGE);
484    }
485
486    /**
487     * Sets a car's destination to a yard track if two trains can route the car.
488     *
489     * @param car the car to be routed
490     * @return true if car's destination has been modified to a yard. False if a
491     *         yard track wasn't found that could service the car's final
492     *         destination.
493     */
494    private boolean setCarDestinationTwoTrainsYard(Car car) {
495        if (Setup.isCarRoutingViaYardsEnabled()) {
496            return setCarDestinationTwoTrains(car, Track.YARD);
497        }
498        return false;
499    }
500
501    /**
502     * Sets a car's destination to a staging track if two trains can route the
503     * car.
504     *
505     * @param car the car to be routed
506     * @return true if car's destination has been modified to a staging track.
507     *         False if a staging track wasn't found that could service the
508     *         car's final destination.
509     */
510    private boolean setCarDestinationTwoTrainsStaging(Car car) {
511        if (Setup.isCarRoutingViaStagingEnabled()) {
512            addLine(BLANK_LINE);
513            addLine(Bundle.getMessage("RouterAttemptStaging", car.toString(),
514                    car.getFinalDestinationName(), car.getFinalDestinationTrackName()));
515            return setCarDestinationTwoTrains(car, Track.STAGING);
516        }
517        return false;
518    }
519
520    /*
521     * Note that this routine loads the last set of tracks and trains that can
522     * service the car to its final location. This routine attempts to find a
523     * "two" train route by cycling through various interchange, yard, and
524     * staging tracks searching for a second train that can pull the car from
525     * the track and deliver the car to the its destination. Then the program
526     * determines if the train being built or another train (first) can deliver
527     * the car to the track from its current location. If successful, a two
528     * train route was found, and returns true.
529     */
530    private boolean setCarDestinationTwoTrains(Car car, String trackType) {
531        Car testCar = clone(car); // reload
532        log.debug("Two train routing, find {} track for car ({}) final destination ({}, {})", trackType, car,
533                testCar.getDestinationName(), testCar.getDestinationTrackName());
534        if (_addtoReportVeryDetailed) {
535            addLine(BLANK_LINE);
536            addLine(Bundle.getMessage("RouterFindTrack", Track.getTrackTypeName(trackType), car.toString(),
537                    testCar.getDestinationName(), testCar.getDestinationTrackName()));
538        }
539        boolean foundRoute = false;
540        // now search for a yard or interchange that a train can pick up and
541        // deliver the car to its destination
542        List<Track> tracks = getTracks(car, testCar, trackType);
543        for (Track track : tracks) {
544            if (_addtoReportVeryDetailed) {
545                addLine(BLANK_LINE);
546                addLine(Bundle.getMessage("RouterFoundTrack",
547                        Track.getTrackTypeName(trackType), track.getLocation().getName(),
548                        track.getName(), car.toString()));
549            }
550            // test to see if there's a train that can deliver the car to its
551            // final location
552            testCar.setTrack(track);
553            testCar.setDestination(car.getFinalDestination());
554            // note that destination track can be null
555            testCar.setDestinationTrack(car.getFinalDestinationTrack());
556            Train secondTrain = trainManager.getTrainForCar(testCar, _excludeTrains, _buildReport, false);
557            if (secondTrain == null) {
558                // maybe the train being built can service the car?
559                String specified = canSpecifiedTrainService(testCar);
560                if (specified.equals(NOT_NOW)) {
561                    secondTrain = _train;
562                } else {
563                    if (_addtoReportVeryDetailed) {
564                        addLine(Bundle.getMessage("RouterNotFindTrain", testCar.toString(),
565                                Track.getTrackTypeName(trackType), track.getLocation().getName(), track.getName(),
566                                testCar.getDestinationName(), testCar.getDestinationTrackName()));
567                    }
568                    continue;
569                }
570            }
571            if (_addtoReportVeryDetailed) {
572                addLine(Bundle.getMessage("RouterTrainCanTransport",
573                        secondTrain.getName(), car.toString(), testCar.getTrack().getTrackTypeName(),
574                        testCar.getLocationName(), testCar.getTrackName(), testCar.getDestinationName(),
575                        testCar.getDestinationTrackName()));
576            }
577            // Save the "last" tracks for later use if needed
578            _lastLocationTracks.add(track);
579            _lastLocationTrains.add(secondTrain);
580            // now try to forward car to this track
581            testCar.setTrack(car.getTrack()); // restore car origin
582            testCar.setDestination(track.getLocation());
583            testCar.setDestinationTrack(track);
584            // determine if car can be transported from current location to this
585            // interchange, yard, or staging track
586            // Now find a train that will transport the car to this track
587            Train firstTrain = null;
588            String specified = canSpecifiedTrainService(testCar);
589            if (specified.equals(YES)) {
590                firstTrain = _train;
591            } else if (specified.equals(NOT_NOW)) {
592                // found a two train route for this car, show the car's route
593                List<Train> trains = new ArrayList<>(Arrays.asList(_train, secondTrain));
594                tracks = new ArrayList<>(Arrays.asList(track, car.getFinalDestinationTrack()));
595                showRoute(car, trains, tracks);
596
597                addLine(Bundle.getMessage("RouterTrainCanNotDueTo",
598                        _train.getName(), car.toString(), track.getLocation().getName(), track.getName(),
599                        _train.getServiceStatus()));
600                foundRoute = true; // issue is route moves or train length
601            } else {
602                firstTrain = trainManager.getTrainForCar(testCar, _excludeTrains, _buildReport, false);
603            }
604            // check to see if a train or trains with the same route is delivering and pulling the car to an interchange track
605            if (firstTrain != null &&
606                    firstTrain.getRoute() == secondTrain.getRoute() &&
607                    track.isInterchange() &&
608                    track.getPickupOption().equals(Track.ANY)) {
609                if (_addtoReportVeryDetailed) {
610                    addLine(Bundle.getMessage("RouterSameInterchange", firstTrain.getName(),
611                            track.getLocation().getName(), track.getName()));
612                }
613                List<Train> excludeTrains = new ArrayList<>(Arrays.asList(firstTrain));
614                firstTrain = trainManager.getTrainForCar(testCar, excludeTrains, _buildReport, true);
615            }
616            if (firstTrain == null && _addtoReportVeryDetailed) {
617                addLine(Bundle.getMessage("RouterNotFindTrain", testCar.toString(),
618                        testCar.getTrack().getTrackTypeName(), testCar.getTrack().getLocation().getName(),
619                        testCar.getTrack().getName(), testCar.getDestinationName(), testCar.getDestinationTrackName()));
620            }
621            // Can the specified train carry this car out of staging?
622            if (_train != null && car.getTrack().isStaging() && !specified.equals(YES)) {
623                if (_addtoReport) {
624                    addLine(Bundle.getMessage("RouterTrainCanNot",
625                            _train.getName(), car.toString(), car.getLocationName(),
626                            car.getTrackName(), track.getLocation().getName(), track.getName()));
627                }
628                continue; // can't use this train
629            }
630            // Is the option for the specified train carry this car?
631            if (firstTrain != null &&
632                    _train != null &&
633                    _train.isServiceAllCarsWithFinalDestinationsEnabled() &&
634                    !specified.equals(YES)) {
635                if (_addtoReport) {
636                    addLine(Bundle.getMessage("RouterOptionToCarry",
637                            _train.getName(), firstTrain.getName(), car.toString(),
638                            track.getLocation().getName(), track.getName()));
639                }
640                continue; // can't use this train
641            }
642            if (firstTrain != null) {
643                foundRoute = true; // found a route
644                if (_addtoReportVeryDetailed) {
645                    addLine(Bundle.getMessage("RouterTrainCanTransport", firstTrain.getName(), car.toString(),
646                            testCar.getTrack().getTrackTypeName(),
647                            testCar.getLocationName(), testCar.getTrackName(), testCar.getDestinationName(),
648                            testCar.getDestinationTrackName()));
649                }
650                // found a two train route for this car, show the car's route
651                List<Train> trains = new ArrayList<>(Arrays.asList(firstTrain, secondTrain));
652                tracks = new ArrayList<>(Arrays.asList(track, car.getFinalDestinationTrack()));
653                showRoute(car, trains, tracks);
654
655                _status = car.checkDestination(track.getLocation(), track);
656                if (_status.startsWith(Track.LENGTH)) {
657                    // if the issue is length at the interim track, add message
658                    // to build report
659                    addLine(Bundle.getMessage("RouterCanNotDeliverCar",
660                            car.toString(), track.getLocation().getName(), track.getName(),
661                            _status, track.getTrackTypeName()));
662                    continue;
663                }
664                if (_status.equals(Track.OKAY)) {
665                    // only set car's destination if specified train can service
666                    // car
667                    if (_train != null && _train != firstTrain) {
668                        addLine(Bundle.getMessage("TrainDoesNotServiceCar",
669                                _train.getName(), car.toString(), testCar.getDestinationName(),
670                                testCar.getDestinationTrackName()));
671                        _status = MessageFormat.format(STATUS_NOT_THIS_TRAIN, new Object[]{firstTrain.getName()});
672                        continue;// found a route but it doesn't start with the
673                                 // specified train
674                    }
675                    // is this the staging track assigned to the specified
676                    // train?
677                    if (track.isStaging() &&
678                            firstTrain.getTerminationTrack() != null &&
679                            firstTrain.getTerminationTrack() != track) {
680                        addLine(Bundle.getMessage("RouterTrainIntoStaging", firstTrain.getName(),
681                                firstTrain.getTerminationTrack().getLocation().getName(),
682                                firstTrain.getTerminationTrack().getName()));
683                        continue;
684                    }
685                    _status = car.setDestination(track.getLocation(), track);
686                    if (_addtoReport) {
687                        addLine(Bundle.getMessage("RouterTrainCanService",
688                                firstTrain.getName(), car.toString(), car.getLocationName(), car.getTrackName(),
689                                Track.getTrackTypeName(trackType), track.getLocation().getName(), track.getName()));
690                    }
691                    return true; // the specified train and another train can
692                                 // carry the car to its destination
693                }
694            }
695        }
696        if (foundRoute) {
697            if (_train != null) {
698                _status = MessageFormat.format(STATUS_NOT_THIS_TRAIN, new Object[]{_train.getName()});
699            } else {
700                _status = STATUS_NOT_ABLE;
701            }
702        }
703        return foundRoute;
704    }
705
706    /**
707     * This routine builds a set of tracks that could be used for routing. It
708     * also lists all of the tracks that can't be used.
709     * 
710     * @param car       The car being routed
711     * @param testCar   the test car
712     * @param trackType the type of track used for routing
713     * @return list of usable tracks
714     */
715    private List<Track> getTracks(Car car, Car testCar, String trackType) {
716        List<Track> inTracks = locationManager.getTracksByMoves(trackType);
717        List<Track> tracks = new ArrayList<Track>();
718        for (Track track : inTracks) {
719            if (car.getTrack() == track || car.getFinalDestinationTrack() == track) {
720                continue; // don't use car's current track
721            }
722            // can't use staging if car's load can be modified
723            if (trackType.equals(Track.STAGING) && track.isModifyLoadsEnabled()) {
724                if (_addtoReportVeryDetailed) {
725                    addLine(Bundle.getMessage("RouterStagingExcluded",
726                            track.getLocation().getName(), track.getName()));
727                }
728                continue;
729            }
730            String status = track.isRollingStockAccepted(testCar);
731            if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
732                if (_addtoReportVeryDetailed) {
733                    addLine(Bundle.getMessage("RouterCanNotDeliverCar",
734                            car.toString(), track.getLocation().getName(), track.getName(),
735                            status, track.getTrackTypeName()));
736                }
737                continue;
738            }
739            tracks.add(track);
740        }
741        return tracks;
742    }
743
744    /*
745     * Note that "last" set of location/tracks (_lastLocationTracks) was loaded
746     * by setCarDestinationTwoTrains. The following code builds two additional
747     * sets of location/tracks called "next" (_nextLocationTracks) and "other"
748     * (_otherLocationTracks). "next" is the next set of location/tracks that
749     * the car can reach by a single train. "last" is the last set of
750     * location/tracks that services the cars final destination. And "other" is
751     * the remaining sets of location/tracks that are not "next" or "last". The
752     * code then tries to connect the "next" and "last" location/track sets with
753     * a train that can service the car. If successful, that would be a three
754     * train route for the car. If not successful, the code than tries
755     * combinations of "next", "other" and "last" location/tracks to create a
756     * route for the car.
757     */
758    private boolean setCarDestinationMultipleTrains(Car car, boolean useStaging) {
759        if (useStaging && !Setup.isCarRoutingViaStagingEnabled())
760            return false; // routing via staging is disabled
761
762        if (_addtoReportVeryDetailed) {
763            addLine(BLANK_LINE);
764        }
765        if (_lastLocationTracks.isEmpty()) {
766            if (useStaging) {
767                addLine(Bundle.getMessage("RouterCouldNotFindStaging",
768                        car.getFinalDestinationName()));
769            } else {
770                addLine(Bundle.getMessage("RouterCouldNotFindLast",
771                        car.getFinalDestinationName()));
772            }
773            return false;
774        }
775
776        Car testCar = clone(car); // reload
777        // build the "next" and "other" location/tracks
778        if (_nextLocationTracks.isEmpty() && _otherLocationTracks.isEmpty()) {
779            loadInterchangeAndYards(car, testCar);
780        }
781        // add staging if requested
782        if (useStaging) {
783            loadStaging(car, testCar);
784        }
785
786        if (_nextLocationTracks.isEmpty()) {
787            addLine(Bundle.getMessage("RouterCouldNotFindLoc",
788                    car.getLocationName()));
789            return false;
790        }
791
792        addLine(Bundle.getMessage("RouterTwoTrainsFailed", car));
793
794        if (_addtoReport) {
795            // tracks that could be the very next destination for the car
796            for (Track t : _nextLocationTracks) {
797                addLine(Bundle.getMessage("RouterNextTrack", t.getTrackTypeName(), t.getLocation().getName(),
798                        t.getName(), car, car.getLocationName(), car.getTrackName(),
799                        _nextLocationTrains.get(_nextLocationTracks.indexOf(t))));
800            }
801            // tracks that could be the next to last destination for the car
802            for (Track t : _lastLocationTracks) {
803                addLine(Bundle.getMessage("RouterLastTrack",
804                        t.getTrackTypeName(), t.getLocation().getName(), t.getName(), car,
805                        car.getFinalDestinationName(), car.getFinalDestinationTrackName(),
806                        _lastLocationTrains.get(_lastLocationTracks.indexOf(t))));
807            }
808        }
809        if (_addtoReportVeryDetailed) {
810            // tracks that are not the next or the last list
811            for (Track t : _otherLocationTracks) {
812                addLine(Bundle.getMessage("RouterOtherTrack", t.getTrackTypeName(), t.getLocation().getName(),
813                        t.getName(), car));
814            }
815            addLine(BLANK_LINE);
816        }
817        boolean foundRoute = routeUsing3Trains(car);
818        if (!foundRoute) {
819            log.debug("Using 3 trains to route car to ({}) was unsuccessful", car.getFinalDestinationName());
820            foundRoute = routeUsing4Trains(car);
821        }
822        if (!foundRoute) {
823            log.debug("Using 4 trains to route car to ({}) was unsuccessful", car.getFinalDestinationName());
824            foundRoute = routeUsing5Trains(car);
825        }
826        if (!foundRoute) {
827            log.debug("Using 5 trains to route car to ({}) was unsuccessful", car.getFinalDestinationName());
828            foundRoute = routeUsing6Trains(car);
829        }
830        if (!foundRoute) {
831            log.debug("Using 6 trains to route car to ({}) was unsuccessful", car.getFinalDestinationName());
832            foundRoute = routeUsing7Trains(car);
833        }
834        if (!foundRoute) {
835            addLine(Bundle.getMessage("RouterNotAbleToRoute", car.toString(), car.getLocationName(),
836                    car.getTrackName(), car.getFinalDestinationName(), car.getFinalDestinationTrackName()));
837        }
838        return foundRoute;
839    }
840
841    private void loadInterchangeAndYards(Car car, Car testCar) {
842        List<Track> tracks;
843        // start with interchanges
844        tracks = locationManager.getTracksByMoves(Track.INTERCHANGE);
845        loadTracksAndTrains(car, testCar, tracks);
846        // next load yards if enabled
847        if (Setup.isCarRoutingViaYardsEnabled()) {
848            tracks = locationManager.getTracksByMoves(Track.YARD);
849            loadTracksAndTrains(car, testCar, tracks);
850        }
851    }
852
853    private void loadStaging(Car car, Car testCar) {
854        // add staging if requested
855        List<Track> stagingTracks = locationManager.getTracksByMoves(Track.STAGING);
856        List<Track> tracks = new ArrayList<Track>();
857        for (Track staging : stagingTracks) {
858            if (!staging.isModifyLoadsEnabled()) {
859                tracks.add(staging);
860            }
861        }
862        loadTracksAndTrains(car, testCar, tracks);
863    }
864
865    private boolean routeUsing3Trains(Car car) {
866        addLine(Bundle.getMessage("RouterNTrains", "3", car.getFinalDestinationName(),
867                car.getFinalDestinationTrackName()));
868        Car testCar = clone(car); // reload
869        boolean foundRoute = false;
870        for (Track nlt : _nextLocationTracks) {
871            for (Track llt : _lastLocationTracks) {
872                // does a train service these two locations?
873                Train middleTrain =
874                        getTrainForCar(testCar, nlt, llt, _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)),
875                                _lastLocationTrains.get(_lastLocationTracks.indexOf(llt)));
876                if (middleTrain != null) {
877                    log.debug("Found 3 train route, setting car destination ({}, {})", nlt.getLocation().getName(),
878                            nlt.getName());
879                    foundRoute = true;
880                    // show the car's route by building an ordered list of
881                    // trains and tracks
882                    List<Train> trains = new ArrayList<>(
883                            Arrays.asList(_nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), middleTrain,
884                                    _lastLocationTrains.get(_lastLocationTracks.indexOf(llt))));
885                    List<Track> tracks = new ArrayList<>(Arrays.asList(nlt, llt, car.getFinalDestinationTrack()));
886                    showRoute(car, trains, tracks);
887                    if (finshSettingRouteFor(car, nlt)) {
888                        return true; // done 3 train routing
889                    }
890                    break; // there was an issue with the first stop in the
891                           // route
892                }
893            }
894        }
895        return tryRedirectToAlternateOrYard(foundRoute, car);
896    }
897
898    private boolean routeUsing4Trains(Car car) {
899        addLine(Bundle.getMessage("RouterNTrains", "4", car.getFinalDestinationName(),
900                car.getFinalDestinationTrackName()));
901        Car testCar = clone(car); // reload
902        boolean foundRoute = false;
903        for (Track nlt : _nextLocationTracks) {
904            otherloop: for (Track mlt : _otherLocationTracks) {
905                Train middleTrain2 = getTrainForCar(testCar, nlt, mlt,
906                        _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), null);
907                if (middleTrain2 == null) {
908                    continue;
909                }
910                // build a list of tracks that are reachable from the 1st
911                // interchange
912                if (!_next2ndLocationTracks.contains(mlt)) {
913                    _next2ndLocationTracks.add(mlt);
914                    if (_addtoReport) {
915                        addLine(Bundle.getMessage("RouterNextHop", mlt.getTrackTypeName(), mlt.getLocation().getName(),
916                                mlt.getName(), car, nlt.getLocation().getName(), nlt.getName(),
917                                middleTrain2.getName()));
918                    }
919                }
920                for (Track llt : _lastLocationTracks) {
921                    Train middleTrain3 = getTrainForCar(testCar, mlt, llt, middleTrain2,
922                            _lastLocationTrains.get(_lastLocationTracks.indexOf(llt)));
923                    if (middleTrain3 == null) {
924                        continue;
925                    }
926                    log.debug("Found 4 train route, setting car destination ({}, {})", nlt.getLocation().getName(),
927                            nlt.getName());
928                    foundRoute = true;
929                    // show the car's route by building an ordered list of
930                    // trains and tracks
931                    List<Train> trains = new ArrayList<>(
932                            Arrays.asList(_nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), middleTrain2,
933                                    middleTrain3, _lastLocationTrains.get(_lastLocationTracks.indexOf(llt))));
934                    List<Track> tracks = new ArrayList<>(Arrays.asList(nlt, mlt, llt, car.getFinalDestinationTrack()));
935                    showRoute(car, trains, tracks);
936                    if (finshSettingRouteFor(car, nlt)) {
937                        return true; // done 4 train routing
938                    }
939                    break otherloop; // there was an issue with the first
940                                     // stop in the route
941                }
942            }
943        }
944        return tryRedirectToAlternateOrYard(foundRoute, car);
945    }
946
947    private boolean routeUsing5Trains(Car car) {
948        addLine(Bundle.getMessage("RouterNTrains", "5", car.getFinalDestinationName(),
949                car.getFinalDestinationTrackName()));
950        Car testCar = clone(car); // reload
951        boolean foundRoute = false;
952        for (Track nlt : _nextLocationTracks) {
953            otherloop: for (Track mlt1 : _next2ndLocationTracks) {
954                Train middleTrain2 = getTrainForCar(testCar, nlt, mlt1,
955                        _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), null);
956                if (middleTrain2 == null) {
957                    continue;
958                }
959                for (Track mlt2 : _otherLocationTracks) {
960                    if (_next2ndLocationTracks.contains(mlt2)) {
961                        continue;
962                    }
963                    Train middleTrain3 = getTrainForCar(testCar, mlt1, mlt2, middleTrain2, null);
964                    if (middleTrain3 == null) {
965                        continue;
966                    }
967                    // build a list of tracks that are reachable from the 2nd
968                    // interchange
969                    if (!_next3rdLocationTracks.contains(mlt2)) {
970                        _next3rdLocationTracks.add(mlt2);
971                        if (_addtoReport) {
972                            addLine(Bundle.getMessage("RouterNextHop", mlt2.getTrackTypeName(),
973                                    mlt2.getLocation().getName(),
974                                    mlt2.getName(), car, mlt1.getLocation().getName(), mlt1.getName(),
975                                    middleTrain3.getName()));
976                        }
977                    }
978                    for (Track llt : _lastLocationTracks) {
979                        Train middleTrain4 = getTrainForCar(testCar, mlt2, llt, middleTrain3,
980                                _lastLocationTrains.get(_lastLocationTracks.indexOf(llt)));
981                        if (middleTrain4 == null) {
982                            continue;
983                        }
984                        log.debug("Found 5 train route, setting car destination ({}, {})",
985                                nlt.getLocation().getName(),
986                                nlt.getName());
987                        foundRoute = true;
988                        // show the car's route by building an ordered list
989                        // of trains and tracks
990                        List<Train> trains = new ArrayList<>(Arrays.asList(
991                                _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), middleTrain2, middleTrain3,
992                                middleTrain4, _lastLocationTrains.get(_lastLocationTracks.indexOf(llt))));
993                        List<Track> tracks =
994                                new ArrayList<>(Arrays.asList(nlt, mlt1, mlt2, llt, car.getFinalDestinationTrack()));
995                        showRoute(car, trains, tracks);
996                        if (finshSettingRouteFor(car, nlt)) {
997                            return true; // done 5 train routing
998                        }
999                        break otherloop; // there was an issue with the
1000                                         // first stop in the route
1001                    }
1002                }
1003            }
1004        }
1005        return tryRedirectToAlternateOrYard(foundRoute, car);
1006    }
1007
1008    private boolean routeUsing6Trains(Car car) {
1009        addLine(Bundle.getMessage("RouterNTrains", "6", car.getFinalDestinationName(),
1010                car.getFinalDestinationTrackName()));
1011        Car testCar = clone(car); // reload
1012        boolean foundRoute = false;
1013        for (Track nlt : _nextLocationTracks) {
1014            otherloop: for (Track mlt1 : _next2ndLocationTracks) {
1015                Train middleTrain2 = getTrainForCar(testCar, nlt, mlt1,
1016                        _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), null);
1017                if (middleTrain2 == null) {
1018                    continue;
1019                }
1020                for (Track mlt2 : _next3rdLocationTracks) {
1021                    Train middleTrain3 = getTrainForCar(testCar, mlt1, mlt2, middleTrain2, null);
1022                    if (middleTrain3 == null) {
1023                        continue;
1024                    }
1025                    for (Track mlt3 : _otherLocationTracks) {
1026                        if (_next2ndLocationTracks.contains(mlt3) || _next3rdLocationTracks.contains(mlt3)) {
1027                            continue;
1028                        }
1029                        Train middleTrain4 = getTrainForCar(testCar, mlt2, mlt3, middleTrain3, null);
1030                        if (middleTrain4 == null) {
1031                            continue;
1032                        }
1033                        if (!_next4thLocationTracks.contains(mlt3)) {
1034                            _next4thLocationTracks.add(mlt3);
1035                            if (_addtoReport) {
1036                                addLine(Bundle.getMessage("RouterNextHop", mlt3.getTrackTypeName(),
1037                                        mlt3.getLocation().getName(), mlt3.getName(), car, mlt2.getLocation().getName(),
1038                                        mlt2.getName(), middleTrain4.getName()));
1039                            }
1040                        }
1041                        for (Track llt : _lastLocationTracks) {
1042                            Train middleTrain5 = getTrainForCar(testCar, mlt3, llt, middleTrain4,
1043                                    _lastLocationTrains.get(_lastLocationTracks.indexOf(llt)));
1044                            if (middleTrain5 == null) {
1045                                continue;
1046                            }
1047                            log.debug("Found 6 train route, setting car destination ({}, {})",
1048                                    nlt.getLocation().getName(), nlt.getName());
1049                            foundRoute = true;
1050                            // show the car's route by building an ordered
1051                            // list of trains and tracks
1052                            List<Train> trains = new ArrayList<>(
1053                                    Arrays.asList(_nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)),
1054                                            middleTrain2, middleTrain3, middleTrain4, middleTrain5,
1055                                            _lastLocationTrains.get(_lastLocationTracks.indexOf(llt))));
1056                            List<Track> tracks = new ArrayList<>(
1057                                    Arrays.asList(nlt, mlt1, mlt2, mlt3, llt, car.getFinalDestinationTrack()));
1058                            showRoute(car, trains, tracks);
1059                            // only set car's destination if specified train
1060                            // can service car
1061                            if (finshSettingRouteFor(car, nlt)) {
1062                                return true; // done 6 train routing
1063                            }
1064                            break otherloop; // there was an issue with the
1065                                             // first stop in the route
1066                        }
1067                    }
1068                }
1069            }
1070        }
1071        return tryRedirectToAlternateOrYard(foundRoute, car);
1072    }
1073
1074    private boolean routeUsing7Trains(Car car) {
1075        addLine(Bundle.getMessage("RouterNTrains", "7", car.getFinalDestinationName(),
1076                car.getFinalDestinationTrackName()));
1077        Car testCar = clone(car); // reload
1078        boolean foundRoute = false;
1079        for (Track nlt : _nextLocationTracks) {
1080            otherloop: for (Track mlt1 : _next2ndLocationTracks) {
1081                Train middleTrain2 = getTrainForCar(testCar, nlt, mlt1,
1082                        _nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)), null);
1083                if (middleTrain2 == null) {
1084                    continue;
1085                }
1086                for (Track mlt2 : _next3rdLocationTracks) {
1087                    Train middleTrain3 = getTrainForCar(testCar, mlt1, mlt2, middleTrain2, null);
1088                    if (middleTrain3 == null) {
1089                        continue;
1090                    }
1091                    for (Track mlt3 : _next4thLocationTracks) {
1092                        Train middleTrain4 = getTrainForCar(testCar, mlt2, mlt3, middleTrain3, null);
1093                        if (middleTrain4 == null) {
1094                            continue;
1095                        }
1096                        for (Track mlt4 : _otherLocationTracks) {
1097                            if (_next2ndLocationTracks.contains(mlt4) ||
1098                                    _next3rdLocationTracks.contains(mlt4) ||
1099                                    _next4thLocationTracks.contains(mlt4)) {
1100                                continue;
1101                            }
1102                            Train middleTrain5 = getTrainForCar(testCar, mlt3, mlt4, middleTrain4, null);
1103                            if (middleTrain5 == null) {
1104                                continue;
1105                            }
1106                            for (Track llt : _lastLocationTracks) {
1107                                Train middleTrain6 = getTrainForCar(testCar, mlt4, llt, middleTrain5,
1108                                        _lastLocationTrains.get(_lastLocationTracks.indexOf(llt)));
1109                                if (middleTrain6 == null) {
1110                                    continue;
1111                                }
1112                                log.debug("Found 7 train route, setting car destination ({}, {})",
1113                                        nlt.getLocation().getName(), nlt.getName());
1114                                foundRoute = true;
1115                                // show the car's route by building an ordered
1116                                // list of trains and tracks
1117                                List<Train> trains = new ArrayList<>(
1118                                        Arrays.asList(_nextLocationTrains.get(_nextLocationTracks.indexOf(nlt)),
1119                                                middleTrain2, middleTrain3, middleTrain4, middleTrain5, middleTrain6,
1120                                                _lastLocationTrains.get(_lastLocationTracks.indexOf(llt))));
1121                                List<Track> tracks = new ArrayList<>(Arrays.asList(nlt, mlt1, mlt2, mlt3, mlt4, llt,
1122                                        car.getFinalDestinationTrack()));
1123                                showRoute(car, trains, tracks);
1124                                // only set car's destination if specified train
1125                                // can service car
1126                                if (finshSettingRouteFor(car, nlt)) {
1127                                    return true; // done 7 train routing
1128                                }
1129                                break otherloop; // there was an issue with the
1130                                                 // first stop in the route
1131                            }
1132                        }
1133                    }
1134                }
1135            }
1136        }
1137        return tryRedirectToAlternateOrYard(foundRoute, car);
1138    }
1139
1140    /**
1141     * This method returns a train that is able to move the test car between the
1142     * fromTrack and the toTrack. The default for an interchange track is to not
1143     * allow the same train to spot and pull a car.
1144     * 
1145     * @param testCar   test car
1146     * @param fromTrack departure track
1147     * @param toTrack   arrival track
1148     * @param fromTrain train servicing fromTrack (previous drop to fromTrack)
1149     * @param toTrain   train servicing toTrack (pulls from the toTrack)
1150     * @return null if no train found, else a train able to move test car
1151     *         between fromTrack and toTrack.
1152     */
1153    private Train getTrainForCar(Car testCar, Track fromTrack, Track toTrack, Train fromTrain, Train toTrain) {
1154        testCar.setTrack(fromTrack); // car to this location and track
1155        testCar.setDestinationTrack(toTrack); // car to this destination & track
1156        List<Train> excludeTrains = new ArrayList<>();
1157        if (fromTrack.isInterchange() && fromTrack.getPickupOption().equals(Track.ANY)) {
1158            excludeTrains.add(fromTrain);
1159        }
1160        if (toTrack.isInterchange() && toTrack.getPickupOption().equals(Track.ANY)) {
1161            excludeTrains.add(toTrain);
1162        }
1163        // does a train service these two locations? 
1164        String key = fromTrack.getId() + toTrack.getId();
1165        Train train = _listTrains.get(key);
1166        if (train == null) {
1167            train = trainManager.getTrainForCar(testCar, excludeTrains, null, true);
1168            if (train != null) {
1169                _listTrains.put(key, train);
1170            } else {
1171                _listTrains.put(key, new Train("null", "null"));
1172            }
1173        } else if (train.getId().equals("null")) {
1174            return null;
1175        }
1176        return train;
1177
1178    }
1179
1180    private void showRoute(Car car, List<Train> trains, List<Track> tracks) {
1181        StringBuffer buf = new StringBuffer(
1182                Bundle.getMessage("RouterRouteForCar", car.toString(), car.getLocationName(), car.getTrackName()));
1183        StringBuffer bufRp = new StringBuffer(
1184                Bundle.getMessage("RouterRoutePath", car.getLocationName(), car.getTrackName()));
1185        for (Track track : tracks) {
1186            if (_addtoReport) {
1187                buf.append(Bundle.getMessage("RouterRouteTrain", trains.get(tracks.indexOf(track)).getName()));
1188            }
1189            bufRp.append(Bundle.getMessage("RouterRoutePathTrain", trains.get(tracks.indexOf(track)).getName()));
1190            if (track != null) {
1191                buf.append(Bundle.getMessage("RouterRouteTrack", track.getLocation().getName(), track.getName()));
1192                bufRp.append(
1193                        Bundle.getMessage("RouterRoutePathTrack", track.getLocation().getName(), track.getName()));
1194            } else {
1195                buf.append(Bundle.getMessage("RouterRouteTrack", car.getFinalDestinationName(),
1196                        car.getFinalDestinationTrackName()));
1197                bufRp.append(Bundle.getMessage("RouterRoutePathTrack", car.getFinalDestinationName(),
1198                        car.getFinalDestinationTrackName()));
1199            }
1200        }
1201        car.setRoutePath(bufRp.toString());
1202        addLine(buf.toString());
1203    }
1204
1205    /**
1206     * @param car   The car to which the destination (track) is going to be
1207     *              applied. Will set car's destination if specified train can
1208     *              service car
1209     * @param track The destination track for car
1210     * @return false if there's an issue with the destination track length or
1211     *         wrong track into staging, otherwise true.
1212     */
1213    private boolean finshSettingRouteFor(Car car, Track track) {
1214        // only set car's destination if specified train can service car
1215        Car ts2 = clone(car);
1216        ts2.setDestinationTrack(track);
1217        String specified = canSpecifiedTrainService(ts2);
1218        if (specified.equals(NO)) {
1219            addLine(Bundle.getMessage("TrainDoesNotServiceCar",
1220                    _train.getName(), car.toString(), track.getLocation().getName(), track.getName()));
1221            _status = MessageFormat.format(STATUS_NOT_THIS_TRAIN, new Object[]{_train.getName()});
1222            return false;
1223        } else if (specified.equals(NOT_NOW)) {
1224            addLine(Bundle.getMessage("RouterTrainCanNotDueTo", _train.getName(), car.toString(),
1225                    track.getLocation().getName(), track.getName(), _train.getServiceStatus()));
1226            return false; // the issue is route moves or train length
1227        }
1228        // check to see if track is staging
1229        if (track.isStaging() &&
1230                _train != null &&
1231                _train.getTerminationTrack() != null &&
1232                _train.getTerminationTrack() != track) {
1233            addLine(Bundle.getMessage("RouterTrainIntoStaging",
1234                    _train.getName(), _train.getTerminationTrack().getLocation().getName(),
1235                    _train.getTerminationTrack().getName()));
1236            return false; // wrong track into staging
1237        }
1238        _status = car.setDestination(track.getLocation(), track);
1239        if (!_status.equals(Track.OKAY)) {
1240            addLine(Bundle.getMessage("RouterCanNotDeliverCar", car.toString(),
1241                    track.getLocation().getName(), track.getName(), _status, track.getTrackTypeName()));
1242            return false;
1243        }
1244        return true;
1245    }
1246
1247    // sets clone car destination to final destination and track
1248    private Car clone(Car car) {
1249        Car clone = car.copy();
1250        // modify clone car length if car is part of kernel
1251        if (car.getKernel() != null) {
1252            clone.setLength(Integer.toString(car.getKernel().getTotalLength() - RollingStock.COUPLERS));
1253        }
1254        clone.setTrack(car.getTrack());
1255        // don't set the clone's final destination track, that will record the
1256        // car as being inbound
1257        clone.setFinalDestinationTrack(null);
1258        // next two items is where the clone is different
1259        clone.setDestination(car.getFinalDestination());
1260        // note that final destination track can be null
1261        clone.setDestinationTrack(car.getFinalDestinationTrack());
1262        return clone;
1263    }
1264
1265    /*
1266     * Creates two sets of tracks when routing. 1st set (_nextLocationTracks) is
1267     * one hop away from car's current location. 2nd set is all other tracks
1268     * (_otherLocationTracks) that aren't one hop away from car's current
1269     * location or destination. Also creates the list of trains used to service
1270     * _nextLocationTracks.
1271     */
1272    private void loadTracksAndTrains(Car car, Car testCar, List<Track> tracks) {
1273        for (Track track : tracks) {
1274            if (track == car.getTrack()) {
1275                continue; // don't use car's current track
1276            }
1277            // note that last could equal next if this routine was used for two
1278            // train routing
1279            if (_lastLocationTracks.contains(track)) {
1280                continue;
1281            }
1282            String status = track.isRollingStockAccepted(testCar);
1283            if (!status.equals(Track.OKAY) && !status.startsWith(Track.LENGTH)) {
1284                continue; // track doesn't accept this car
1285            }
1286            // test to see if there's a train that can deliver the car to this
1287            // destination
1288            testCar.setDestinationTrack(track);
1289            Train train = null;
1290            String specified = canSpecifiedTrainService(testCar);
1291            if (specified.equals(YES) || specified.equals(NOT_NOW)) {
1292                train = _train;
1293            } else {
1294                train = trainManager.getTrainForCar(testCar, null);
1295            }
1296            // Can specified train carry this car out of staging?
1297            if (car.getTrack().isStaging() && specified.equals(NO)) {
1298                train = null;
1299            }
1300            // is the option carry all cars with a final destination enabled?
1301            if (train != null &&
1302                    _train != null &&
1303                    _train != train &&
1304                    _train.isServiceAllCarsWithFinalDestinationsEnabled() &&
1305                    !specified.equals(YES)) {
1306                addLine(Bundle.getMessage("RouterOptionToCarry", _train.getName(),
1307                        train.getName(), car.toString(), track.getLocation().getName(), track.getName()));
1308                train = null;
1309            }
1310            if (train != null) {
1311                _nextLocationTracks.add(track);
1312                _nextLocationTrains.add(train);
1313            } else {
1314                _otherLocationTracks.add(track);
1315            }
1316        }
1317    }
1318
1319    private static final String NO = "no"; // NOI18N
1320    private static final String YES = "yes"; // NOI18N
1321    private static final String NOT_NOW = "not now"; // NOI18N
1322    private static final String NO_SPECIFIED_TRAIN = "no specified train"; // NOI18N
1323
1324    private String canSpecifiedTrainService(Car car) {
1325        if (_train == null) {
1326            return NO_SPECIFIED_TRAIN;
1327        }
1328        if (_train.isServiceable(car)) {
1329            return YES;
1330        } // is the reason this train can't service route moves or train length?
1331        else if (!_train.getServiceStatus().equals(Train.NONE)) {
1332            return NOT_NOW; // the issue is route moves or train length
1333        }
1334        return NO;
1335    }
1336
1337    /**
1338     * Used when the 1st hop interchanges and yards are full. Will attempt to
1339     * use a spur's alternate track when pulling a car from the spur. This will
1340     * create a local move. Code checks to see if local move by the train being
1341     * used is allowed. If alternate track isn't available, then try yard
1342     * tracks.
1343     * 
1344     * @param car the car being redirected
1345     * @return true if car's destination was set to alternate track
1346     */
1347    private boolean tryRedirectToAlternateOrYard(boolean foundRoute, Car car) {
1348        if (foundRoute) {
1349            if (car.getTrack().getAlternateTrack() != null) {
1350                // try redirecting car to the alternate track
1351                Car ts = clone(car);
1352                ts.setDestinationTrack(car.getTrack().getAlternateTrack());
1353                String specified = canSpecifiedTrainService(ts);
1354                if (specified.equals(YES)) {
1355                    String status = car.setDestination(car.getTrack().getAlternateTrack().getLocation(),
1356                            car.getTrack().getAlternateTrack());
1357                    if (status.equals(Track.OKAY)) {
1358                        addLine(Bundle.getMessage("RouterSendCarToAlternative",
1359                                car.toString(), car.getTrack().getAlternateTrack().getName(),
1360                                car.getTrack().getAlternateTrack().getLocation().getName()));
1361                        return true;
1362                    } else {
1363                        addLine(Bundle.getMessage("RouterAlternateFailed",
1364                                car.getTrack().getAlternateTrack().getName(), status));
1365                    }
1366                }
1367            }
1368            // try moving the car to yard tracks
1369            if (!car.getTrack().getTrackType().equals(Track.YARD) && Setup.isForwardToYardEnabled()) {
1370                Car ts = clone(car);
1371                List<Track> yards = car.getLocation().getTracksByMoves(Track.YARD);
1372                log.debug("Found {} yard(s) at location ({})", yards.size(), car.getLocationName());
1373                for (Track track : yards) {
1374                    ts.setDestinationTrack(track);
1375                    String specified = canSpecifiedTrainService(ts);
1376                    if (specified.equals(YES)) {
1377                        String status = car.setDestination(track.getLocation(), track);
1378                        if (status.equals(Track.OKAY)) {
1379                            addLine(Bundle.getMessage("RouterSendCarToYard", car.toString(),
1380                                    track.getLocation().getName(),
1381                                    track.getName()));
1382                            return true; // car is going to a yard
1383                        } else {
1384                            addLine(Bundle.getMessage("RouterCanNotUseYard", track.getLocation().getName(),
1385                                    track.getName(),
1386                                    status));
1387                        }
1388                    }
1389                }
1390                addLine(Bundle.getMessage("RouterNoYardTracks", car.getLocationName(), car.toString()));
1391            }
1392        }
1393        return foundRoute;
1394    }
1395
1396    private static final Logger log = LoggerFactory.getLogger(Router.class);
1397
1398    // all router build report messages are at level seven
1399    protected void addLine(String string) {
1400        addLine(_buildReport, SEVEN, string);
1401    }
1402
1403}