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