001package jmri.jmrit.operations.trains;
002
003import java.io.*;
004import java.nio.charset.StandardCharsets;
005import java.text.MessageFormat;
006import java.util.*;
007
008import javax.swing.JOptionPane;
009
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013import jmri.InstanceManager;
014import jmri.Version;
015import jmri.jmrit.operations.locations.*;
016import jmri.jmrit.operations.locations.schedules.ScheduleItem;
017import jmri.jmrit.operations.rollingstock.RollingStock;
018import jmri.jmrit.operations.rollingstock.cars.*;
019import jmri.jmrit.operations.rollingstock.engines.Engine;
020import jmri.jmrit.operations.rollingstock.engines.EngineManager;
021import jmri.jmrit.operations.router.Router;
022import jmri.jmrit.operations.routes.RouteLocation;
023import jmri.jmrit.operations.setup.Setup;
024import jmri.jmrit.operations.trains.schedules.TrainSchedule;
025import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
026
027/**
028 * Methods to support the TrainBuilder class.
029 * 
030 * @author Daniel Boudreau Copyright (C) 2021
031 *
032 */
033public class TrainBuilderBase extends TrainCommon {
034
035    // report levels
036    protected static final String ONE = Setup.BUILD_REPORT_MINIMAL;
037    protected static final String THREE = Setup.BUILD_REPORT_NORMAL;
038    protected static final String FIVE = Setup.BUILD_REPORT_DETAILED;
039    protected static final String SEVEN = Setup.BUILD_REPORT_VERY_DETAILED;
040
041    protected static final int DISPLAY_CAR_LIMIT_20 = 20; // build exception out of staging
042    protected static final int DISPLAY_CAR_LIMIT_50 = 50;
043    protected static final int DISPLAY_CAR_LIMIT_100 = 100;
044
045    protected static final boolean USE_BUNIT = true;
046
047    // build variables shared between local routines
048    Date _startTime; // when the build report started
049    Train _train; // the train being built
050    int _numberCars = 0; // number of cars moved by this train
051    List<Engine> _engineList; // list of engines available for this train, modified during the build
052    Engine _lastEngine; // last engine found from getEngine
053    Engine _secondLeadEngine; // the lead engine in the second half of the train's route
054    Engine _thirdLeadEngine; // the lead engine in the third part of the train's route
055    int _carIndex; // index for carList
056    List<Car> _carList; // list of cars available for this train, modified during the build
057    List<RouteLocation> _routeList; // list of locations from departure to termination served by this train
058    Hashtable<String, Integer> _numOfBlocks; // Number of blocks of cars departing staging.
059    int _completedMoves; // the number of pick up car moves for a location
060    int _reqNumOfMoves; // the requested number of car moves for a location
061    Location _departLocation; // train departs this location
062    Track _departStageTrack; // departure staging track (null if not staging)
063    Location _terminateLocation; // train terminates at this location
064    Track _terminateStageTrack; // terminate staging track (null if not staging)
065    boolean _success; // true when enough cars have been picked up from a location
066    PrintWriter _buildReport; // build report for this train
067    List<Car> _notRoutable = new ArrayList<>(); // list of cars that couldn't be routed
068    List<Location> _modifiedLocations = new ArrayList<>(); // list of locations that have been modified
069    int _warnings = 0; // the number of warnings in the build report
070
071    // managers
072    CarManager carManager = InstanceManager.getDefault(CarManager.class);
073    LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
074    EngineManager engineManager = InstanceManager.getDefault(EngineManager.class);
075    TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
076    TrainScheduleManager trainScheduleManager = InstanceManager.getDefault(TrainScheduleManager.class);
077    CarLoads carLoads = InstanceManager.getDefault(CarLoads.class);
078    Router router = InstanceManager.getDefault(Router.class);
079
080    protected void createBuildReportFile() {
081        // backup the train's previous build report file
082        InstanceManager.getDefault(TrainManagerXml.class).savePreviousBuildStatusFile(_train.getName());
083
084        // create build report file
085        File file = InstanceManager.getDefault(TrainManagerXml.class).createTrainBuildReportFile(_train.getName());
086        try {
087            _buildReport = new PrintWriter(
088                    new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)),
089                    true);
090        } catch (IOException e) {
091            log.error("Can not open build report file: {}", file.getName());
092            return;
093        }
094    }
095
096    /**
097     * Creates the build report header information lines. Build report date, JMRI
098     * version, train schedule, build report display levels, setup comment.
099     */
100    protected void showBuildReportInfo() {
101        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("BuildReportMsg"),
102                new Object[] { _train.getName(), _startTime }));
103        addLine(_buildReport, ONE,
104                MessageFormat.format(Bundle.getMessage("BuildReportVersion"), new Object[] { Version.name() }));
105        if (!trainScheduleManager.getTrainScheduleActiveId().equals(TrainScheduleManager.NONE)) {
106            if (trainScheduleManager.getTrainScheduleActiveId().equals(TrainSchedule.ANY)) {
107                addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildActiveSchedule"),
108                        new Object[] { Bundle.getMessage("Any") }));
109            } else {
110                TrainSchedule sch = trainScheduleManager.getActiveSchedule();
111                if (sch != null) {
112                    addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildActiveSchedule"),
113                            new Object[] { sch.getName() }));
114                }
115            }
116        }
117        // show the various build detail levels
118        addLine(_buildReport, THREE, Bundle.getMessage("buildReportLevelThree"));
119        addLine(_buildReport, FIVE, Bundle.getMessage("buildReportLevelFive"));
120        addLine(_buildReport, SEVEN, Bundle.getMessage("buildReportLevelSeven"));
121
122        if (Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_DETAILED)) {
123            addLine(_buildReport, SEVEN, Bundle.getMessage("buildRouterReportLevelDetailed"));
124        } else if (Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_VERY_DETAILED)) {
125            addLine(_buildReport, SEVEN, Bundle.getMessage("buildRouterReportLevelVeryDetailed"));
126        }
127
128        if (!Setup.getComment().trim().isEmpty()) {
129            addLine(_buildReport, ONE, BLANK_LINE);
130            addLine(_buildReport, ONE, Setup.getComment());
131        }
132        addLine(_buildReport, ONE, BLANK_LINE);
133    }
134
135    protected void setUpRoute() throws BuildFailedException {
136        if (_train.getRoute() == null) {
137            throw new BuildFailedException(
138                    MessageFormat.format(Bundle.getMessage("buildErrorRoute"), new Object[] { _train.getName() }));
139        }
140        // get the train's route
141        _routeList = _train.getRoute().getLocationsBySequenceList();
142        if (_routeList.size() < 1) {
143            throw new BuildFailedException(
144                    MessageFormat.format(Bundle.getMessage("buildErrorNeedRoute"), new Object[] { _train.getName() }));
145        }
146        // train departs
147        _departLocation = locationManager.getLocationByName(_train.getTrainDepartsName());
148        if (_departLocation == null) {
149            throw new BuildFailedException(
150                    MessageFormat.format(Bundle.getMessage("buildErrorNeedDepLoc"), new Object[] { _train.getName() }));
151        }
152        // train terminates
153        _terminateLocation = locationManager.getLocationByName(_train.getTrainTerminatesName());
154        if (_terminateLocation == null) {
155            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorNeedTermLoc"),
156                    new Object[] { _train.getName() }));
157        }
158    }
159
160    /**
161     * show train build options when in detailed mode
162     */
163    protected void showTrainBuildOptions() {
164        ResourceBundle rb = ResourceBundle.getBundle("jmri.jmrit.operations.setup.JmritOperationsSetupBundle");
165        addLine(_buildReport, FIVE, Bundle.getMessage("MenuItemBuildOptions") + ":");
166        if (Setup.isBuildAggressive()) {
167            addLine(_buildReport, FIVE, Bundle.getMessage("BuildModeAggressive"));
168            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("BuildNumberPasses"),
169                    new Object[] { Setup.getNumberPasses() }));
170            if (Setup.isStagingTrackImmediatelyAvail() && _departLocation.isStaging()) {
171                addLine(_buildReport, FIVE, Bundle.getMessage("BuildStagingTrackAvail"));
172            }
173        } else {
174            addLine(_buildReport, FIVE, Bundle.getMessage("BuildModeNormal"));
175        }
176        // show switcher options
177        if (_train.isLocalSwitcher()) {
178            addLine(_buildReport, FIVE, BLANK_LINE);
179            addLine(_buildReport, FIVE, rb.getString("BorderLayoutSwitcherService") + ":");
180            if (Setup.isLocalInterchangeMovesEnabled()) {
181                addLine(_buildReport, FIVE, rb.getString("AllowLocalInterchange"));
182            } else {
183                addLine(_buildReport, FIVE, rb.getString("NoAllowLocalInterchange"));
184            }
185            if (Setup.isLocalSpurMovesEnabled()) {
186                addLine(_buildReport, FIVE, rb.getString("AllowLocalSpur"));
187            } else {
188                addLine(_buildReport, FIVE, rb.getString("NoAllowLocalSpur"));
189            }
190            if (Setup.isLocalYardMovesEnabled()) {
191                addLine(_buildReport, FIVE, rb.getString("AllowLocalYard"));
192            } else {
193                addLine(_buildReport, FIVE, rb.getString("NoAllowLocalYard"));
194            }
195        }
196        // show staging options
197        if (_departLocation.isStaging() || _terminateLocation.isStaging()) {
198            addLine(_buildReport, FIVE, BLANK_LINE);
199            addLine(_buildReport, FIVE, Bundle.getMessage("buildStagingOptions"));
200
201            if (Setup.isStagingTrainCheckEnabled() && _terminateLocation.isStaging()) {
202                addLine(_buildReport, FIVE, Bundle.getMessage("buildOptionRestrictStaging"));
203            }
204            if (Setup.isStagingTrackImmediatelyAvail() && _terminateLocation.isStaging()) {
205                addLine(_buildReport, FIVE, rb.getString("StagingAvailable"));
206            }
207            if (Setup.isStagingAllowReturnEnabled() &&
208                    _departLocation.isStaging() &&
209                    _terminateLocation.isStaging() &&
210                    _departLocation == _terminateLocation) {
211                addLine(_buildReport, FIVE, rb.getString("AllowCarsToReturn"));
212            }
213            if (Setup.isStagingPromptFromEnabled() && _departLocation.isStaging()) {
214                addLine(_buildReport, FIVE, rb.getString("PromptFromStaging"));
215            }
216            if (Setup.isStagingPromptToEnabled() && _terminateLocation.isStaging()) {
217                addLine(_buildReport, FIVE, rb.getString("PromptToStaging"));
218            }
219            if (Setup.isStagingTryNormalBuildEnabled() && _departLocation.isStaging()) {
220                addLine(_buildReport, FIVE, rb.getString("TryNormalStaging"));
221            }
222        }
223
224        // Car routing options
225        addLine(_buildReport, FIVE, BLANK_LINE);
226        addLine(_buildReport, FIVE, Bundle.getMessage("buildCarRoutingOptions"));
227
228        // warn if car routing is disabled
229        if (!Setup.isCarRoutingEnabled()) {
230            addLine(_buildReport, FIVE, Bundle.getMessage("RoutingDisabled"));
231            _warnings++;
232        } else {
233            if (Setup.isCarRoutingViaYardsEnabled()) {
234                addLine(_buildReport, FIVE, Bundle.getMessage("RoutingViaYardsEnabled"));
235            }
236            if (Setup.isCarRoutingViaStagingEnabled()) {
237                addLine(_buildReport, FIVE, Bundle.getMessage("RoutingViaStagingEnabled"));
238            }
239            if (Setup.isOnlyActiveTrainsEnabled()) {
240                addLine(_buildReport, FIVE, Bundle.getMessage("OnlySelectedTrains"));
241                _warnings++;
242                // list the selected trains
243                for (Train train : trainManager.getTrainsByNameList()) {
244                    if (train.isBuildEnabled()) {
245                        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainNameAndDesc"),
246                                new Object[] { train.getName(), train.getDescription() }));
247                    }
248                }
249                if (!_train.isBuildEnabled()) {
250                    addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainNotSelected"),
251                            new Object[] { _train.getName() }));
252                }
253            } else {
254                addLine(_buildReport, FIVE, rb.getString("AllTrains"));
255            }
256            if (Setup.isCheckCarDestinationEnabled()) {
257                addLine(_buildReport, FIVE, Bundle.getMessage("CheckCarDestination"));
258            }
259        }
260        addLine(_buildReport, FIVE, BLANK_LINE);
261    }
262
263    /*
264     * Show the enabled and disabled build options for this train.
265     */
266    protected void showSpecificTrainBuildOptions() {
267        addLine(_buildReport, FIVE,
268                MessageFormat.format(Bundle.getMessage("buildOptionsForTrain"), new Object[] { _train.getName() }));
269        showSpecificTrainBuildOptions(true);
270        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildDisabledOptionsForTrain"),
271                new Object[] { _train.getName() }));
272        showSpecificTrainBuildOptions(false);
273    }
274
275    /*
276     * Enabled when true lists selected build options for this train. Enabled when
277     * false list disabled build options for this train.
278     */
279    private void showSpecificTrainBuildOptions(boolean enabled) {
280
281        if (_train.isBuildTrainNormalEnabled() ^ !enabled) {
282            addLine(_buildReport, FIVE, Bundle.getMessage("NormalModeWhenBuilding"));
283        }
284        if (_train.isSendCarsToTerminalEnabled() ^ !enabled) {
285            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("SendToTerminal"),
286                    new Object[] { _terminateLocation.getName() }));
287        }
288        if ((_train.isAllowReturnToStagingEnabled() || Setup.isStagingAllowReturnEnabled()) ^ !enabled &&
289                _departLocation.isStaging() &&
290                _departLocation == _terminateLocation) {
291            addLine(_buildReport, FIVE, Bundle.getMessage("AllowCarsToReturn"));
292        }
293        if (_train.isAllowLocalMovesEnabled() ^ !enabled) {
294            addLine(_buildReport, FIVE, Bundle.getMessage("AllowLocalMoves"));
295        }
296        if (_train.isAllowThroughCarsEnabled() ^ !enabled && _departLocation != _terminateLocation) {
297            addLine(_buildReport, FIVE, Bundle.getMessage("AllowThroughCars"));
298        }
299        if (_train.isServiceAllCarsWithFinalDestinationsEnabled() ^ !enabled) {
300            addLine(_buildReport, FIVE, Bundle.getMessage("ServiceAllCars"));
301        }
302        if (_train.isSendCarsWithCustomLoadsToStagingEnabled() ^ !enabled) {
303            addLine(_buildReport, FIVE, Bundle.getMessage("SendCustomToStaging"));
304        }
305        if (_train.isBuildConsistEnabled() ^ !enabled) {
306            addLine(_buildReport, FIVE, Bundle.getMessage("BuildConsist"));
307            if (enabled) {
308                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("BuildConsistHPT"),
309                        new Object[] { Setup.getHorsePowerPerTon() }));
310            }
311        }
312        addLine(_buildReport, FIVE, BLANK_LINE);
313    }
314
315    /**
316     * Adds to the build report what the train will service. Road and owner names,
317     * built dates, and engine types.
318     */
319    protected void showTrainServices() {
320        // show road names that this train will service
321        if (!_train.getLocoRoadOption().equals(Train.ALL_ROADS)) {
322            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainLocoRoads"), new Object[] {
323                    _train.getName(), _train.getLocoRoadOption(), formatStringToCommaSeparated(_train.getLocoRoadNames()) }));
324        }
325        // show owner names that this train will service
326        if (!_train.getOwnerOption().equals(Train.ALL_OWNERS)) {
327            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainOwners"), new Object[] {
328                    _train.getName(), _train.getOwnerOption(), formatStringToCommaSeparated(_train.getOwnerNames()) }));
329        }
330        // show built dates serviced
331        if (!_train.getBuiltStartYear().equals(Train.NONE)) {
332            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainBuiltAfter"),
333                    new Object[] { _train.getName(), _train.getBuiltStartYear() }));
334        }
335        if (!_train.getBuiltEndYear().equals(Train.NONE)) {
336            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainBuiltBefore"),
337                    new Object[] { _train.getName(), _train.getBuiltEndYear() }));
338        }
339
340        // show engine types that this train will service
341        if (!_train.getNumberEngines().equals("0")) {
342            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainServicesEngineTypes"),
343                    new Object[] { _train.getName() }));
344            addLine(_buildReport, FIVE, formatStringToCommaSeparated(_train.getLocoTypeNames()));
345        }
346    }
347
348    /**
349     * Show and initialize the train's route. Determines the number of car moves
350     * requested for this train. Also adjust the number of car moves if the random
351     * car moves option was selected.
352     * 
353     * @throws BuildFailedException if random variable isn't an integer
354     */
355    protected void showAndInitializeTrainRoute() throws BuildFailedException {
356        int requestedCarMoves = 0; // how many cars were asked to be moved
357        // TODO: DAB control minimal build by each train
358
359        addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildTrainRoute"),
360                new Object[] { _train.getName(), _train.getRoute().getName() }));
361
362        // get the number of requested car moves for this train
363        for (RouteLocation rl : _routeList) {
364            // check to see if there's a location for each stop in the route
365            Location location = locationManager.getLocationByName(rl.getName()); // this checks for a deleted location
366            if (location == null || rl.getLocation() == null) {
367                throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorLocMissing"),
368                        new Object[] { _train.getRoute().getName() }));
369            }
370            // train doesn't drop or pick up cars from staging locations found in middle of
371            // a route
372            if (location.isStaging() &&
373                    rl != _train.getTrainDepartsRouteLocation() &&
374                    rl != _train.getTrainTerminatesRouteLocation()) {
375                addLine(_buildReport, ONE,
376                        MessageFormat.format(Bundle.getMessage("buildLocStaging"), new Object[] { rl.getName() }));
377                rl.setCarMoves(rl.getMaxCarMoves()); // don't allow car moves for this location
378                // if a location is skipped, no car drops or pick ups
379            } else if (_train.isLocationSkipped(rl.getId())) {
380                addLine(_buildReport, THREE,
381                        MessageFormat.format(Bundle.getMessage("buildLocSkippedMaxTrain"),
382                                new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), _train.getName(), rl.getMaxTrainLength(),
383                                        Setup.getLengthUnit().toLowerCase() }));
384                rl.setCarMoves(rl.getMaxCarMoves()); // don't allow car moves for this location
385            } else if (!rl.isDropAllowed() && !rl.isPickUpAllowed()) {
386                addLine(_buildReport, THREE,
387                        MessageFormat.format(Bundle.getMessage("buildLocNoDropsOrPickups"), new Object[] { rl.getId(),
388                                rl.getName(), rl.getTrainDirectionString(), rl.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase() }));
389                rl.setCarMoves(rl.getMaxCarMoves()); // don't allow car moves for this location
390            } else {
391                // we're going to use this location, so initialize the route location
392                rl.setCarMoves(0); // clear the number of moves
393                requestedCarMoves += rl.getMaxCarMoves(); // add up the total number of car moves requested
394                // show the type of moves allowed at this location
395                if (location.isStaging() && rl.isPickUpAllowed() && rl == _train.getTrainDepartsRouteLocation()) {
396                    addLine(_buildReport, THREE,
397                            MessageFormat.format(Bundle.getMessage("buildStagingDeparts"),
398                                    new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves(),
399                                            rl.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase() }));
400                } else if (location.isStaging() &&
401                        rl.isDropAllowed() &&
402                        rl == _train.getTrainTerminatesRouteLocation()) {
403                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingTerminates"),
404                            new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves() }));
405                } else if (rl == _train.getTrainTerminatesRouteLocation() &&
406                        rl.isDropAllowed() &&
407                        rl.isPickUpAllowed()) {
408                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildLocTerminatesMoves"),
409                            new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves() }));
410                } else if (rl.isDropAllowed() && rl.isPickUpAllowed()) {
411                    addLine(_buildReport, THREE,
412                            MessageFormat.format(Bundle.getMessage("buildLocRequestMoves"),
413                                    new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves(),
414                                            rl.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase() }));
415                } else if (!rl.isDropAllowed()) {
416                    addLine(_buildReport, THREE,
417                            MessageFormat.format(Bundle.getMessage("buildLocRequestPickups"),
418                                    new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves(),
419                                            rl.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase() }));
420                } else if (rl == _train.getTrainTerminatesRouteLocation()) {
421                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildLocTerminates"),
422                            new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves() }));
423                } else {
424                    addLine(_buildReport, THREE,
425                            MessageFormat.format(Bundle.getMessage("buildLocRequestDrops"),
426                                    new Object[] { rl.getId(), rl.getName(), rl.getTrainDirectionString(), rl.getMaxCarMoves(),
427                                            rl.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase() }));
428                }
429            }
430            rl.setTrainWeight(0); // clear the total train weight
431            rl.setTrainLength(0); // and length
432        }
433
434        // check for random moves in the train's route
435        for (RouteLocation rl : _routeList) {
436            if (rl.getRandomControl().equals(RouteLocation.DISABLED)) {
437                continue;
438            }
439            if (rl.getCarMoves() == 0 && rl.getMaxCarMoves() > 0) {
440                log.debug("Location ({}) has random control value {} and maximum moves {}", rl.getName(),
441                        rl.getRandomControl(), rl.getMaxCarMoves());
442                try {
443                    int value = Integer.parseInt(rl.getRandomControl());
444                    // now adjust the number of available moves for this location
445                    double random = Math.random();
446                    log.debug("random {}", random);
447                    int moves = (int) (random * ((rl.getMaxCarMoves() * value / 100) + 1));
448                    log.debug("Reducing number of moves for location ({}) by {}", rl.getName(), moves);
449                    rl.setCarMoves(moves);
450                    requestedCarMoves = requestedCarMoves - moves;
451                    addLine(_buildReport, FIVE,
452                            MessageFormat.format(Bundle.getMessage("buildRouteRandomControl"),
453                                    new Object[] { rl.getName(), rl.getId(), rl.getRandomControl(), rl.getMaxCarMoves(),
454                                            rl.getMaxCarMoves() - moves }));
455                } catch (NumberFormatException e) {
456                    throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorRandomControl"),
457                            new Object[] { _train.getRoute().getName(), rl.getName(), rl.getRandomControl() }));
458                }
459            }
460        }
461
462        int numMoves = requestedCarMoves; // number of car moves
463        if (!_train.isLocalSwitcher()) {
464            requestedCarMoves = requestedCarMoves / 2; // only need half as many cars to meet requests
465        }
466        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildRouteRequest"), new Object[] {
467                _train.getRoute().getName(), Integer.toString(requestedCarMoves), Integer.toString(numMoves) }));
468
469        _train.setNumberCarsRequested(requestedCarMoves); // save number of car moves requested
470        addLine(_buildReport, ONE, BLANK_LINE);
471    }
472
473    /**
474     * reports if local switcher
475     */
476    protected void showIfLocalSwitcher() {
477        if (_train.isLocalSwitcher()) {
478            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildTrainIsSwitcher"),
479                    new Object[] { _train.getName(), TrainCommon.splitString(_train.getTrainDepartsName()) }));
480            addLine(_buildReport, THREE, BLANK_LINE);
481        }
482    }
483
484    /**
485     * Show how many engines are required for this train, and if a certain road name
486     * for the engine is requested. Show if there are any engine changes in the
487     * route, or if helper engines are needed. There can be up to 2 engine changes
488     * or helper requests. Show if caboose or FRED is needed for train, and if
489     * there's a road name requested. There can be up to 2 caboose changes in the
490     * route.
491     */
492    protected void showTrainRequirements() {
493        addLine(_buildReport, ONE, Bundle.getMessage("TrainRequirements"));
494        if (_train.getNumberEngines().equals("0")) {
495            addLine(_buildReport, ONE, Bundle.getMessage("buildTrainReq0Engine"));
496        } else if (_train.getNumberEngines().equals("1")) {
497            addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildTrainReq1Engine"),
498                    new Object[] { _train.getTrainDepartsName(), _train.getEngineModel(), _train.getEngineRoad() }));
499        } else {
500            addLine(_buildReport, ONE,
501                    MessageFormat.format(Bundle.getMessage("buildTrainReqEngine"),
502                            new Object[] { _train.getTrainDepartsName(), _train.getNumberEngines(),
503                                    _train.getEngineModel(), _train.getEngineRoad() }));
504        }
505        // show any required loco changes
506        if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
507            addLine(_buildReport, ONE,
508                    MessageFormat.format(Bundle.getMessage("buildTrainEngineChange"),
509                            new Object[] { _train.getSecondLegStartLocationName(), _train.getSecondLegNumberEngines(),
510                                    _train.getSecondLegEngineModel(), _train.getSecondLegEngineRoad() }));
511        }
512        if ((_train.getSecondLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
513            addLine(_buildReport, ONE,
514                    MessageFormat.format(Bundle.getMessage("buildTrainHelperEngines"),
515                            new Object[] { _train.getSecondLegNumberEngines(), _train.getSecondLegStartLocationName(),
516                                    _train.getSecondLegEndLocationName(), _train.getSecondLegEngineModel(),
517                                    _train.getSecondLegEngineRoad() }));
518        }
519        if ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
520            addLine(_buildReport, ONE,
521                    MessageFormat.format(Bundle.getMessage("buildTrainEngineChange"),
522                            new Object[] { _train.getThirdLegStartLocationName(), _train.getThirdLegNumberEngines(),
523                                    _train.getThirdLegEngineModel(), _train.getThirdLegEngineRoad() }));
524        }
525        if ((_train.getThirdLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
526            addLine(_buildReport, ONE,
527                    MessageFormat.format(Bundle.getMessage("buildTrainHelperEngines"),
528                            new Object[] { _train.getThirdLegNumberEngines(), _train.getThirdLegStartLocationName(),
529                                    _train.getThirdLegEndLocationName(), _train.getThirdLegEngineModel(),
530                                    _train.getThirdLegEngineRoad() }));
531        }
532        // show caboose or FRED requirements
533        if (_train.isCabooseNeeded()) {
534            addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildTrainRequiresCaboose"),
535                    new Object[] { _train.getTrainDepartsName(), _train.getCabooseRoad() }));
536        }
537        // show any caboose changes in the train's route
538        if ((_train.getSecondLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
539                (_train.getSecondLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
540            addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildCabooseChange"),
541                    new Object[] { _train.getSecondLegStartRouteLocation() }));
542        }
543        if ((_train.getThirdLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
544                (_train.getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
545            addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildCabooseChange"),
546                    new Object[] { _train.getThirdLegStartRouteLocation() }));
547        }
548        if (_train.isFredNeeded()) {
549            addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildTrainRequiresFRED"),
550                    new Object[] { _train.getTrainDepartsName(), _train.getCabooseRoad() }));
551        }
552        addLine(_buildReport, ONE, BLANK_LINE);
553    }
554
555 
556    /**
557     * Will also set the termination track if returning to staging
558     * 
559     * @param departStageTrack departure track from staging
560     */
561    protected void setDepartureTrack(Track departStageTrack) {
562        if ((_terminateStageTrack == null || _terminateStageTrack == _departStageTrack) &&
563                _departLocation == _terminateLocation &&
564                Setup.isBuildAggressive() &&
565                Setup.isStagingTrackImmediatelyAvail()) {
566            _terminateStageTrack = departStageTrack; // use the same track
567        }
568        _departStageTrack = departStageTrack;
569    }
570
571
572    
573    protected void showTrainCarRoads() {
574        if (!_train.getCarRoadOption().equals(Train.ALL_ROADS)) {
575            addLine(_buildReport, FIVE, BLANK_LINE);
576            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainRoads"), new Object[]{
577                    _train.getName(), _train.getCarRoadOption(),
578                    formatStringToCommaSeparated(_train.getCarRoadNames())}));
579        }
580    }
581
582    protected void showTrainCarTypes() {
583        addLine(_buildReport, FIVE, BLANK_LINE);
584        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainServicesCarTypes"),
585                new Object[] { _train.getName() }));
586        addLine(_buildReport, FIVE, formatStringToCommaSeparated(_train.getCarTypeNames()));
587    }
588
589    protected void showTrainLoadNames() {
590        if (!_train.getLoadOption().equals(Train.ALL_LOADS)) {
591            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildTrainLoads"), new Object[] {
592                    _train.getName(), _train.getLoadOption(), formatStringToCommaSeparated(_train.getLoadNames()) }));
593        }
594    }
595
596    /**
597     * Ask which staging track the train is to depart on.
598     *
599     * @return The departure track the user selected.
600     */
601    protected Track promptFromStagingDialog() {
602        List<Track> tracksIn = _departLocation.getTracksByNameList(null);
603        List<Track> validTracks = new ArrayList<>();
604        // only show valid tracks
605        for (Track track : tracksIn) {
606            if (checkDepartureStagingTrack(track)) {
607                validTracks.add(track);
608            }
609        }
610        if (validTracks.size() > 1) {
611            // need an object array for dialog window
612            Object[] tracks = new Object[validTracks.size()];
613            for (int i = 0; i < validTracks.size(); i++) {
614                tracks[i] = validTracks.get(i);
615            }
616
617            Track selected = (Track) JOptionPane.showInputDialog(null,
618                    MessageFormat.format(Bundle.getMessage("TrainDepartingStaging"),
619                            new Object[] { _train.getName(), _departLocation.getName() }),
620                    Bundle.getMessage("SelectDepartureTrack"), JOptionPane.QUESTION_MESSAGE, null, tracks, null);
621            if (selected != null) {
622                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildUserSelectedDeparture"),
623                        new Object[] { selected.getName(), selected.getLocation().getName() }));
624            }
625            return selected;
626        } else if (validTracks.size() == 1) {
627            Track track = validTracks.get(0);
628            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildOnlyOneDepartureTrack"),
629                    new Object[] { track.getName(), track.getLocation().getName() }));
630            return track;
631        }
632        return null; // no tracks available
633    }
634
635    /**
636     * Ask which staging track the train is to terminate on.
637     *
638     * @return The termination track selected by the user.
639     */
640    protected Track promptToStagingDialog() {
641        List<Track> tracksIn = _terminateLocation.getTracksByNameList(null);
642        List<Track> validTracks = new ArrayList<>();
643        // only show valid tracks
644        for (Track track : tracksIn) {
645            if (checkTerminateStagingTrack(track)) {
646                validTracks.add(track);
647            }
648        }
649        if (validTracks.size() > 1) {
650            // need an object array for dialog window
651            Object[] tracks = new Object[validTracks.size()];
652            for (int i = 0; i < validTracks.size(); i++) {
653                tracks[i] = validTracks.get(i);
654            }
655
656            Track selected = (Track) JOptionPane.showInputDialog(null,
657                    MessageFormat.format(Bundle.getMessage("TrainTerminatingStaging"),
658                            new Object[] { _train.getName(), _terminateLocation.getName() }),
659                    Bundle.getMessage("SelectArrivalTrack"), JOptionPane.QUESTION_MESSAGE, null, tracks, null);
660            if (selected != null) {
661                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildUserSelectedArrival"),
662                        new Object[] { selected.getName(), selected.getLocation().getName() }));
663            }
664            return selected;
665        } else if (validTracks.size() == 1) {
666            return validTracks.get(0);
667        }
668        return null; // no tracks available
669    }
670
671
672
673    /**
674     * Removes the remaining cabooses and cars with FRED from consideration.
675     * 
676     * @throws BuildFailedException code check if car being removed is in staging
677     */
678    protected void removeCaboosesAndCarsWithFred() throws BuildFailedException {
679        addLine(_buildReport, SEVEN, BLANK_LINE);
680        addLine(_buildReport, SEVEN, Bundle.getMessage("buildRemoveCarsNotNeeded"));
681        for (int i = 0; i < _carList.size(); i++) {
682            Car car = _carList.get(i);
683            if (car.isCaboose() || car.hasFred()) {
684                addLine(_buildReport, SEVEN,
685                        MessageFormat.format(Bundle.getMessage("buildExcludeCarTypeAtLoc"),
686                                new Object[] { car.toString(), car.getTypeName(),
687                                        car.getLocationName(), car.getTrackName() }));
688                // code check, should never be staging
689                if (car.getTrack() == _departStageTrack) {
690                    throw new BuildFailedException("ERROR: Attempt to removed car with FRED or Caboose from staging"); // NOI18N
691                }
692                _carList.remove(car); // remove this car from the list
693                i--;
694            }
695        }
696    }
697
698    /**
699     * Save the car's final destination and schedule id in case of train reset
700     */
701    protected void saveCarFinalDestinations() {
702        for (Car car : _carList) {
703            car.setPreviousFinalDestination(car.getFinalDestination());
704            car.setPreviousFinalDestinationTrack(car.getFinalDestinationTrack());
705            car.setPreviousScheduleId(car.getScheduleItemId());
706        }
707    }
708
709    /**
710     * Creates the carList. Only cars that can be serviced by this train are in the
711     * list.
712     * 
713     * @throws BuildFailedException if car is marked as missing and is in staging
714     */
715    protected void getCarList() throws BuildFailedException {
716        // get list of cars for this route
717        _carList = carManager.getAvailableTrainList(_train);
718        // TODO: DAB this needs to be controlled by each train
719
720        addLine(_buildReport, SEVEN, BLANK_LINE);
721        addLine(_buildReport, SEVEN, Bundle.getMessage("buildRemoveCars"));
722        boolean showCar = true;
723        int carListSize = _carList.size();
724        // now remove cars that the train can't service
725        for (int i = 0; i < _carList.size(); i++) {
726            Car car = _carList.get(i);
727            // only show the first 100 cars removed due to wrong car type for train
728            if (showCar && carListSize - _carList.size() == DISPLAY_CAR_LIMIT_100) {
729                showCar = false;
730                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildOnlyFirstXXXCars"),
731                        new Object[] { DISPLAY_CAR_LIMIT_100, Bundle.getMessage("Type") }));
732            }
733            // remove cars that don't have a track assignment
734            if (car.getTrack() == null) {
735                _warnings++;
736                addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildWarningRsNoTrack"),
737                        new Object[] { car.toString(), car.getLocationName() }));
738                _carList.remove(car);
739                i--;
740                continue;
741            }
742            // remove cars that have been reported as missing
743            if (car.isLocationUnknown()) {
744                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarLocUnknown"),
745                        new Object[] { car.toString(), car.getLocationName(), car.getTrackName() }));
746                if (car.getTrack().equals(_departStageTrack)) {
747                    throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorLocationUnknown"),
748                            new Object[] { car.getLocationName(), car.getTrackName(), car.toString() }));
749                }
750                _carList.remove(car);
751                i--;
752                continue;
753            }
754            // remove cars that are out of service
755            if (car.isOutOfService()) {
756                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarOutOfService"),
757                        new Object[] { car.toString(), car.getLocationName(), car.getTrackName() }));
758                if (car.getTrack().equals(_departStageTrack)) {
759                    throw new BuildFailedException(
760                            MessageFormat.format(Bundle.getMessage("buildErrorLocationOutOfService"),
761                                    new Object[] { car.getLocationName(), car.getTrackName(), car.toString() }));
762                }
763                _carList.remove(car);
764                i--;
765                continue;
766            }
767            // does car have a destination that is part of this train's route?
768            if (car.getDestination() != null) {
769                addLine(_buildReport, SEVEN,
770                        MessageFormat.format(Bundle.getMessage("buildCarHasAssignedDest"), new Object[] {
771                                car.toString(), car.getLoadName(), car.getDestinationName(), car.getDestinationTrackName() }));
772                RouteLocation rld = _train.getRoute().getLastLocationByName(car.getDestinationName());
773                if (rld == null) {
774                    addLine(_buildReport, SEVEN, MessageFormat.format(
775                            Bundle.getMessage("buildExcludeCarDestNotPartRoute"),
776                            new Object[] { car.toString(), car.getDestinationName(), _train.getRoute().getName() }));
777                    // Code check, programming ERROR if car departing staging
778                    if (car.getLocation().equals(_departLocation) && _departStageTrack != null) {
779                        throw new BuildFailedException(MessageFormat.format(
780                                Bundle.getMessage("buildErrorCarNotPartRoute"), new Object[] { car.toString() }));
781                    }
782                    _carList.remove(car); // remove this car from the list
783                    i--;
784                    continue;
785                }
786            }
787            // remove cars with FRED that have a destination that isn't the terminal
788            if (car.hasFred() && car.getDestination() != null && car.getDestination() != _terminateLocation) {
789                addLine(_buildReport, FIVE,
790                        MessageFormat.format(Bundle.getMessage("buildExcludeCarWrongDest"),
791                                new Object[] { car.toString(), car.getTypeName(), car.getTypeExtensions(),
792                                        car.getDestinationName() }));
793                _carList.remove(car);
794                i--;
795                continue;
796            }
797
798            // remove cabooses that have a destination that isn't the terminal, no caboose
799            // changes in the train's route
800            if (car.isCaboose() &&
801                    car.getDestination() != null &&
802                    car.getDestination() != _terminateLocation &&
803                    (_train.getSecondLegOptions() & Train.ADD_CABOOSE + Train.REMOVE_CABOOSE) == 0 &&
804                    (_train.getThirdLegOptions() & Train.ADD_CABOOSE + Train.REMOVE_CABOOSE) == 0) {
805                addLine(_buildReport, FIVE,
806                        MessageFormat.format(Bundle.getMessage("buildExcludeCarWrongDest"),
807                                new Object[] { car.toString(), car.getTypeName(), car.getTypeExtensions(),
808                                        car.getDestinationName() }));
809                _carList.remove(car);
810                i--;
811                continue;
812            }
813
814            // is car at interchange?
815            if (car.getTrack().isInterchange()) {
816                // don't service a car at interchange and has been dropped off by this train
817                if (car.getTrack().getPickupOption().equals(Track.ANY) &&
818                        car.getLastRouteId().equals(_train.getRoute().getId())) {
819                    addLine(_buildReport, SEVEN,
820                            MessageFormat.format(Bundle.getMessage("buildExcludeCarDropByTrain"),
821                                    new Object[] { car.toString(), car.getTypeName(), _train.getRoute().getName(), car.getLocationName(),
822                                            car.getTrackName() }));
823                    _carList.remove(car);
824                    i--;
825                    continue;
826                }
827            }
828            // is car at interchange or spur and is this train allowed to pull?
829            if (car.getTrack().isInterchange() || car.getTrack().isSpur()) {
830                if (car.getTrack().getPickupOption().equals(Track.TRAINS) ||
831                        car.getTrack().getPickupOption().equals(Track.EXCLUDE_TRAINS)) {
832                    if (car.getTrack().isPickupTrainAccepted(_train)) {
833                        log.debug("Car ({}) can be picked up by this train", car.toString());
834                    } else {
835                        addLine(_buildReport, SEVEN,
836                                MessageFormat.format(Bundle.getMessage("buildExcludeCarByTrain"),
837                                        new Object[] { car.toString(), car.getTypeName(), car.getTrack().getTrackTypeName(),
838                                                car.getLocationName(), car.getTrackName() }));
839                        _carList.remove(car);
840                        i--;
841                        continue;
842                    }
843                } else if (car.getTrack().getPickupOption().equals(Track.ROUTES) ||
844                        car.getTrack().getPickupOption().equals(Track.EXCLUDE_ROUTES)) {
845                    if (car.getTrack().isPickupRouteAccepted(_train.getRoute())) {
846                        log.debug("Car ({}) can be picked up by this route", car.toString());
847                    } else {
848                        addLine(_buildReport, SEVEN,
849                                MessageFormat.format(Bundle.getMessage("buildExcludeCarByRoute"),
850                                        new Object[] { car.toString(), car.getTypeName(), car.getTrack().getTrackTypeName(),
851                                                car.getLocationName(), car.getTrackName() }));
852                        _carList.remove(car);
853                        i--;
854                        continue;
855                    }
856                }
857            }
858
859            // note that for trains departing staging the engine and car roads, types,
860            // owners, and built date were checked in the routine checkDepartureStagingTrack().
861            if (!_train.isCarRoadNameAccepted(car.getRoadName())) {
862                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarWrongRoad"),
863                        new Object[]{car.toString(), car.getLocationName(), car.getTrackName(), car.getTypeName(),
864                                car.getRoadName()}));
865                _carList.remove(car);
866                i--;
867                continue;
868            }
869            if (!_train.isTypeNameAccepted(car.getTypeName())) {
870                if (showCar) {
871                    addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarWrongType"),
872                            new Object[]{car.toString(), car.getLocationName(), car.getTrackName(),
873                                    car.getTypeName(),}));
874                }
875                _carList.remove(car);
876                i--;
877                continue;
878            }
879            if (!_train.isOwnerNameAccepted(car.getOwnerName())) {
880                addLine(_buildReport, SEVEN,
881                        MessageFormat.format(Bundle.getMessage("buildExcludeCarOwnerAtLoc"), new Object[]{
882                                car.toString(), car.getOwnerName(), car.getLocationName(), car.getTrackName()}));
883                _carList.remove(car);
884                i--;
885                continue;
886            }
887            if (!_train.isBuiltDateAccepted(car.getBuilt())) {
888                addLine(_buildReport, SEVEN,
889                        MessageFormat.format(Bundle.getMessage("buildExcludeCarBuiltAtLoc"),
890                                new Object[]{car.toString(), car.getBuilt(),
891                                        car.getLocationName(), car.getTrackName()}));
892                _carList.remove(car);
893                i--;
894                continue;
895            }
896
897            // all cars in staging must be accepted, so don't exclude if in staging
898            // note that a car's load can change when departing staging
899            // a car's wait value is ignored when departing staging
900            // a car's pick up day is ignored when departing staging
901            if (_departStageTrack == null || car.getTrack() != _departStageTrack) {
902                if (!car.isCaboose() &&
903                        !car.isPassenger() &&
904                        !_train.isLoadNameAccepted(car.getLoadName(), car.getTypeName())) {
905                    addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarLoadAtLoc"),
906                            new Object[] { car.toString(), car.getTypeName(), car.getLoadName() }));
907                    _carList.remove(car);
908                    i--;
909                    continue;
910                }
911                // remove cars with FRED if not needed by train
912                if (car.hasFred() && !_train.isFredNeeded()) {
913                    addLine(_buildReport, SEVEN,
914                            MessageFormat.format(Bundle.getMessage("buildExcludeCarWithFredAtLoc"),
915                                    new Object[] { car.toString(), car.getTypeName(),
916                                            (car.getLocationName() + ", " + car.getTrackName()) }));
917                    _carList.remove(car); // remove this car from the list
918                    i--;
919                    continue;
920                }
921                // does the car have a pick up day?
922                if (!car.getPickupScheduleId().equals(Car.NONE)) {
923                    if (trainScheduleManager.getTrainScheduleActiveId().equals(TrainSchedule.ANY) ||
924                            car.getPickupScheduleId().equals(trainScheduleManager.getTrainScheduleActiveId())) {
925                        car.setPickupScheduleId(Car.NONE);
926                    } else {
927                        TrainSchedule sch = trainScheduleManager.getScheduleById(car.getPickupScheduleId());
928                        if (sch != null) {
929                            addLine(_buildReport, SEVEN,
930                                    MessageFormat.format(Bundle.getMessage("buildExcludeCarSchedule"),
931                                            new Object[] { car.toString(), car.getTypeName(), car.getLocationName(),
932                                                    car.getTrackName(), sch.getName() }));
933                            _carList.remove(car);
934                            i--;
935                            continue;
936                        }
937                    }
938                }
939                // does car have a wait count?
940                if (car.getWait() > 0) {
941                    addLine(_buildReport, SEVEN,
942                            MessageFormat.format(Bundle.getMessage("buildExcludeCarWait"),
943                                    new Object[] { car.toString(), car.getTypeName(), car.getLocationName(),
944                                            car.getTrackName(), car.getWait() }));
945                    if (_train.isServiceable(car)) {
946                        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainCanServiceWait"),
947                                new Object[] { _train.getName(), car.toString(), car.getWait() - 1 }));
948                        car.setWait(car.getWait() - 1); // decrement wait count
949                        // a car's load changes when the wait count reaches 0
950                        String oldLoad = car.getLoadName();
951                        if (car.getTrack().isSpur()) {
952                            car.updateLoad(car.getTrack()); // has the wait count reached 0?
953                        }
954                        String newLoad = car.getLoadName();
955                        if (!oldLoad.equals(newLoad)) {
956                            addLine(_buildReport, SEVEN,
957                                    MessageFormat.format(Bundle.getMessage("buildCarLoadChangedWait"),
958                                            new Object[] { car.toString(), car.getTypeName(), oldLoad, newLoad }));
959                        }
960                    }
961                    _carList.remove(car);
962                    i--;
963                    continue;
964                }
965            }
966        }
967    }
968
969    /**
970     * Adjust car list to only have cars from one staging track
971     * 
972     * @throws BuildFailedException if all cars departing staging can't be used
973     */
974    protected void adjustCarsInStaging() throws BuildFailedException {
975        if (_departStageTrack == null) {
976            return; // not departing staging
977        }
978        int numCarsFromStaging = 0;
979        _numOfBlocks = new Hashtable<>();
980        addLine(_buildReport, SEVEN, BLANK_LINE);
981        addLine(_buildReport, SEVEN, Bundle.getMessage("buildRemoveCarsStaging"));
982        for (int i = 0; i < _carList.size(); i++) {
983            Car car = _carList.get(i);
984            if (car.getLocationName().equals(_departLocation.getName())) {
985                if (car.getTrackName().equals(_departStageTrack.getName())) {
986                    numCarsFromStaging++;
987                    // populate car blocking hashtable
988                    // don't block cabooses, cars with FRED, or passenger. Only block lead cars in
989                    // kernel
990                    if (!car.isCaboose() &&
991                            !car.hasFred() &&
992                            !car.isPassenger() &&
993                            (car.getKernel() == null || car.isLead())) {
994                        log.debug("Car {} last location id: {}", car.toString(), car.getLastLocationId());
995                        Integer number = 1;
996                        if (_numOfBlocks.containsKey(car.getLastLocationId())) {
997                            number = _numOfBlocks.get(car.getLastLocationId()) + 1;
998                            _numOfBlocks.remove(car.getLastLocationId());
999                        }
1000                        _numOfBlocks.put(car.getLastLocationId(), number);
1001                    }
1002                } else {
1003                    addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeCarAtLoc"),
1004                            new Object[] { car.toString(), car.getTypeName(), car.getLocationName(), car.getTrackName() }));
1005                    _carList.remove(car);
1006                    i--;
1007                }
1008            }
1009        }
1010        // show how many cars are departing from staging
1011        addLine(_buildReport, FIVE, BLANK_LINE);
1012        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildDepartingStagingCars"), new Object[] {
1013                _departStageTrack.getLocation().getName(), _departStageTrack.getName(), numCarsFromStaging }));
1014        // and list them
1015        for (Car car : _carList) {
1016            if (car.getTrack() == _departStageTrack) {
1017                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildStagingCarAtLoc"),
1018                        new Object[] { car.toString(), car.getTypeName(), car.getLoadName() }));
1019            }
1020        }
1021        // error if all of the cars from staging aren't available
1022        if (numCarsFromStaging != _departStageTrack.getNumberCars()) {
1023            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorNotAllCars"),
1024                    new Object[] { _departStageTrack.getName(),
1025                            Integer.toString(_departStageTrack.getNumberCars() - numCarsFromStaging) }));
1026        }
1027        log.debug("Staging departure track ({}) has {} cars and {} blocks", _departStageTrack.getName(),
1028                numCarsFromStaging, _numOfBlocks.size()); // NOI18N
1029    }
1030
1031    /**
1032     * List available cars by location. Removes non-lead kernel cars from the car
1033     * list.
1034     * 
1035     * @throws BuildFailedException if kernel doesn't have lead or cars aren't on
1036     *                              the same track.
1037     */
1038    protected void showCarsByLocation() throws BuildFailedException {
1039        // show how many cars were found
1040        addLine(_buildReport, FIVE, BLANK_LINE);
1041        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildFoundCars"),
1042                new Object[] { Integer.toString(_carList.size()), _train.getName() }));
1043
1044        List<String> locationNames = new ArrayList<>(); // only show cars once using the train's route
1045        for (RouteLocation rl : _train.getRoute().getLocationsBySequenceList()) {
1046            if (locationNames.contains(rl.getName())) {
1047                continue;
1048            }
1049            locationNames.add(rl.getName());
1050            if (rl.getLocation().isStaging()) {
1051                addLine(_buildReport, SEVEN,
1052                        MessageFormat.format(Bundle.getMessage("buildCarsInStaging"), new Object[] { rl.getName() }));
1053            } else {
1054                addLine(_buildReport, SEVEN,
1055                        MessageFormat.format(Bundle.getMessage("buildCarsAtLocation"), new Object[] { rl.getName() }));
1056            }
1057            // now go through the car list and remove non-lead cars in kernels, destinations
1058            // that aren't part of this route
1059            int carCount = 0;
1060            for (int i = 0; i < _carList.size(); i++) {
1061                Car car = _carList.get(i);
1062                if (!car.getLocationName().equals(rl.getName())) {
1063                    continue;
1064                }
1065                // only print out the first DISPLAY_CAR_LIMIT cars for each location
1066                if (carCount < DISPLAY_CAR_LIMIT_50 && (car.getKernel() == null || car.isLead())) {
1067                    if (car.getLoadPriority().equals(CarLoad.PRIORITY_LOW)) {
1068                        addLine(_buildReport, SEVEN,
1069                                MessageFormat.format(Bundle.getMessage("buildCarAtLocWithMoves"),
1070                                        new Object[] { car.toString(), car.getTypeName(), car.getTypeExtensions(),
1071                                                car.getLocationName(), car.getTrackName(), car.getMoves() }));
1072                    } else {
1073                        addLine(_buildReport, SEVEN,
1074                                MessageFormat.format(Bundle.getMessage("buildCarAtLocWithMovesPriority"),
1075                                        new Object[] { car.toString(), car.getTypeName(), car.getTypeExtensions(),
1076                                                car.getLocationName(), car.getTrackName(), car.getMoves(),
1077                                                car.getLoadName(), car.getLoadPriority() }));
1078                    }
1079                    if (car.isLead()) {
1080                        addLine(_buildReport, SEVEN,
1081                                MessageFormat.format(Bundle.getMessage("buildCarLeadKernel"),
1082                                        new Object[] { car.toString(), car.getKernelName(), car.getKernel().getSize(),
1083                                                car.getKernel().getTotalLength(),
1084                                                Setup.getLengthUnit().toLowerCase() }));
1085                        // list all of the cars in the kernel now
1086                        for (Car k : car.getKernel().getCars()) {
1087                            if (!k.isLead()) {
1088                                addLine(_buildReport, SEVEN,
1089                                        MessageFormat.format(Bundle.getMessage("buildCarPartOfKernel"),
1090                                                new Object[] { k.toString(), k.getKernelName(), k.getKernel().getSize(),
1091                                                        k.getKernel().getTotalLength(),
1092                                                        Setup.getLengthUnit().toLowerCase() }));
1093                            }
1094                        }
1095                    }
1096                    carCount++;
1097                    if (carCount == DISPLAY_CAR_LIMIT_50) {
1098                        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildOnlyFirstXXXCars"),
1099                                new Object[] { carCount, rl.getName() }));
1100                    }
1101                }
1102                // use only the lead car in a kernel for building trains
1103                if (car.getKernel() != null) {
1104                    checkKernel(car); // kernel needs lead car and all cars on the same track
1105                    if (!car.isLead()) {
1106                        _carList.remove(car); // remove this car from the list
1107                        i--;
1108                        continue;
1109                    }
1110                }
1111                if (_train.equals(car.getTrain())) {
1112                    addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildCarAlreadyAssigned"),
1113                            new Object[] { car.toString() }));
1114                }
1115            }
1116            addLine(_buildReport, SEVEN, BLANK_LINE);
1117        }
1118    }
1119
1120    protected void sortCarsOnFifoLifoTracks() {
1121        addLine(_buildReport, SEVEN, Bundle.getMessage("buildSortCarsByLastDate"));
1122        for (_carIndex = 0; _carIndex < _carList.size(); _carIndex++) {
1123            Car car = _carList.get(_carIndex);
1124            if (car.getTrack().getServiceOrder().equals(Track.NORMAL)) {
1125                continue;
1126            }
1127            addLine(_buildReport, SEVEN,
1128                    MessageFormat.format(Bundle.getMessage("buildTrackModePriority"),
1129                            new Object[] { car.toString(), car.getTrack().getTrackTypeName(), car.getTrackName(),
1130                                    car.getTrack().getServiceOrder(), car.getLastDate() }));
1131            Car bestCar = car;
1132            for (int i = _carIndex + 1; i < _carList.size(); i++) {
1133                Car testCar = _carList.get(i);
1134                if (testCar.getTrack() == car.getTrack()) {
1135                    log.debug("{} car ({}) last moved date: {}", car.getTrack().getTrackTypeName(), testCar.toString(),
1136                            testCar.getLastDate()); // NOI18N
1137                    if (car.getTrack().getServiceOrder().equals(Track.FIFO)) {
1138                        if (bestCar.getLastMoveDate().after(testCar.getLastMoveDate()) &&
1139                                bestCar.getLoadPriority().equals(testCar.getLoadPriority())) {
1140                            bestCar = testCar;
1141                            log.debug("New best car ({})", bestCar.toString());
1142                        }
1143                    } else if (car.getTrack().getServiceOrder().equals(Track.LIFO)) {
1144                        if (bestCar.getLastMoveDate().before(testCar.getLastMoveDate()) &&
1145                                bestCar.getLoadPriority().equals(testCar.getLoadPriority())) {
1146                            bestCar = testCar;
1147                            log.debug("New best car ({})", bestCar.toString());
1148                        }
1149                    }
1150                }
1151            }
1152            if (car != bestCar) {
1153                addLine(_buildReport, SEVEN,
1154                        MessageFormat.format(Bundle.getMessage("buildTrackModeCarPriority"),
1155                                new Object[] { car.getTrack().getTrackTypeName(), car.getTrackName(),
1156                                        car.getTrack().getServiceOrder(), bestCar.toString(), bestCar.getLastDate(),
1157                                        car.toString(), car.getLastDate() }));
1158                _carList.remove(bestCar); // change sort
1159                _carList.add(_carIndex, bestCar);
1160            }
1161        }
1162        addLine(_buildReport, SEVEN, BLANK_LINE);
1163    }
1164
1165    /**
1166     * Verifies that all cars in the kernel have the same departure track. Also
1167     * checks to see if the kernel has a lead car and the lead car is in service.
1168     *
1169     * @throws BuildFailedException
1170     */
1171    private void checkKernel(Car car) throws BuildFailedException {
1172        boolean foundLeadCar = false;
1173        for (Car c : car.getKernel().getCars()) {
1174            // check that lead car exists
1175            if (c.isLead() && !c.isOutOfService()) {
1176                foundLeadCar = true;
1177            }
1178            // check to see that all cars have the same location and track
1179            if (car.getLocation() != c.getLocation() || car.getTrack() != c.getTrack()) {
1180                throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorCarKernelLocation"),
1181                        new Object[] { c.toString(), car.getKernelName(), c.getLocationName(), c.getTrackName(),
1182                                car.toString(), car.getLocationName(), car.getTrackName(), }));
1183            }
1184        }
1185        // code check, all kernels should have a lead car
1186        if (foundLeadCar == false) {
1187            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorCarKernelNoLead"),
1188                    new Object[] { car.getKernelName() }));
1189        }
1190    }
1191
1192    /*
1193     * For blocking cars out of staging
1194     */
1195    protected String getLargestBlock() {
1196        Enumeration<String> en = _numOfBlocks.keys();
1197        String largestBlock = "";
1198        int maxCars = 0;
1199        while (en.hasMoreElements()) {
1200            String locId = en.nextElement();
1201            if (_numOfBlocks.get(locId) > maxCars) {
1202                largestBlock = locId;
1203                maxCars = _numOfBlocks.get(locId);
1204            }
1205        }
1206        return largestBlock;
1207    }
1208
1209    /**
1210     * Returns the routeLocation with the most available moves. Used for blocking a
1211     * train out of staging.
1212     *
1213     * @param blockRouteList The route for this train, modified by deleting
1214     *                       RouteLocations serviced
1215     * @param blockId        Where these cars were originally picked up from.
1216     * @return The location in the route with the most available moves.
1217     */
1218    protected RouteLocation getLocationWithMaximumMoves(List<RouteLocation> blockRouteList, String blockId) {
1219        RouteLocation rlMax = null;
1220        int maxMoves = 0;
1221        for (RouteLocation rl : blockRouteList) {
1222            if (rl == _train.getTrainDepartsRouteLocation()) {
1223                continue;
1224            }
1225            if (rl.getMaxCarMoves() - rl.getCarMoves() > maxMoves) {
1226                maxMoves = rl.getMaxCarMoves() - rl.getCarMoves();
1227                rlMax = rl;
1228            }
1229            // if two locations have the same number of moves, return the one that doesn't
1230            // match the block id
1231            if (rl.getMaxCarMoves() - rl.getCarMoves() == maxMoves && !rl.getLocation().getId().equals(blockId)) {
1232                rlMax = rl;
1233            }
1234        }
1235        return rlMax;
1236    }
1237
1238    /**
1239     * Temporally remove cars from staging track if train returning to the same
1240     * staging track to free up track space.
1241     */
1242    protected void makeAdjustmentsIfDepartingStaging() {
1243        if (_departStageTrack != null) {
1244            _reqNumOfMoves = 0; // Move cars out of staging after working other locations
1245            // if leaving and returning to staging on the same track temporary pull cars off
1246            // the track
1247            if (_departStageTrack == _terminateStageTrack) {
1248                if (!_train.isAllowReturnToStagingEnabled() && !Setup.isStagingAllowReturnEnabled()) {
1249                    // takes care of cars in a kernel by getting all cars
1250                    for (Car car : carManager.getList()) {
1251                        // don't remove caboose or car with FRED already assigned to train
1252                        if (car.getTrack() == _departStageTrack && car.getRouteDestination() == null) {
1253                            car.setLocation(car.getLocation(), null); // still in staging but no track
1254                        }
1255                    }
1256                } else {
1257                    // since all cars can return to staging, the track space is consumed for now
1258                    addLine(_buildReport, THREE, BLANK_LINE);
1259                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildWarnDepartStaging"),
1260                            new Object[] { _departStageTrack.getLocation().getName(), _departStageTrack.getName() }));
1261                    addLine(_buildReport, THREE, BLANK_LINE);
1262                }
1263            }
1264            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildDepartStagingAggressive"),
1265                    new Object[] { _departStageTrack.getLocation().getName() }));
1266        }
1267    }
1268
1269    /**
1270     * Restores cars departing staging track assignment.
1271     */
1272    protected void restoreCarsIfDepartingStaging() {
1273        if (_departStageTrack != null &&
1274                _departStageTrack == _terminateStageTrack &&
1275                !_train.isAllowReturnToStagingEnabled() &&
1276                !Setup.isStagingAllowReturnEnabled()) {
1277            // restore departure track for cars departing staging
1278            for (Car car : _carList) {
1279                if (car.getLocation() == _departStageTrack.getLocation() && car.getTrack() == null) {
1280                    car.setLocation(_departStageTrack.getLocation(), _departStageTrack, RollingStock.FORCE); // force
1281                    if (car.getKernel() != null) {
1282                        for (Car k : car.getKernel().getCars()) {
1283                            k.setLocation(_departStageTrack.getLocation(), _departStageTrack, RollingStock.FORCE); // force
1284                        }
1285                    }
1286                }
1287            }
1288        }
1289    }
1290
1291    protected void showLoadGenerationOptionsStaging() {
1292        if (_departStageTrack != null &&
1293                _reqNumOfMoves > 0 &&
1294                (_departStageTrack.isAddCustomLoadsEnabled() ||
1295                        _departStageTrack.isAddCustomLoadsAnySpurEnabled() ||
1296                        _departStageTrack.isAddCustomLoadsAnyStagingTrackEnabled())) {
1297            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildCustomLoadOptions"),
1298                    new Object[] { _departStageTrack.getName() }));
1299            if (_departStageTrack.isAddCustomLoadsEnabled()) {
1300                addLine(_buildReport, FIVE, Bundle.getMessage("buildLoadCarLoads"));
1301            }
1302            if (_departStageTrack.isAddCustomLoadsAnySpurEnabled()) {
1303                addLine(_buildReport, FIVE, Bundle.getMessage("buildLoadAnyCarLoads"));
1304            }
1305            if (_departStageTrack.isAddCustomLoadsAnyStagingTrackEnabled()) {
1306                addLine(_buildReport, FIVE, Bundle.getMessage("buildLoadsStaging"));
1307            }
1308            addLine(_buildReport, FIVE, BLANK_LINE);
1309        }
1310    }
1311
1312    /**
1313     * Checks to see if all cars on a staging track have been given a destination.
1314     * Throws exception if there's a car without a destination.
1315     * 
1316     * @throws BuildFailedException if car on staging track not assigned to train
1317     */
1318    protected void checkStuckCarsInStaging() throws BuildFailedException {
1319        if (_departStageTrack == null) {
1320            return; // only check departure track after last pass is complete
1321        }
1322        int carCount = 0;
1323        StringBuffer buf = new StringBuffer();
1324        // confirm that all cars in staging are departing
1325        for (Car car : _carList) {
1326            // build failure if car departing staging without a destination or train
1327            if (car.getTrack() == _departStageTrack &&
1328                    (car.getDestination() == null || car.getDestinationTrack() == null || car.getTrain() == null)) {
1329                if (car.getKernel() != null) {
1330                    for (Car c : car.getKernel().getCars()) {
1331                        carCount++;
1332                        addCarToStuckStagingList(c, buf, carCount);
1333                    }
1334                } else {
1335                    carCount++;
1336                    addCarToStuckStagingList(car, buf, carCount);
1337                }
1338            }
1339        }
1340        if (carCount > 0) {
1341            log.debug("{} cars stuck in staging", carCount);
1342            String msg = MessageFormat.format(Bundle.getMessage("buildStagingCouldNotFindDest"),
1343                    new Object[] { carCount, _departStageTrack.getLocation().getName(), _departStageTrack.getName() });
1344            throw new BuildFailedException(msg + buf.toString(), BuildFailedException.STAGING);
1345        }
1346    }
1347
1348    /**
1349     * Creates a list of up to 20 cars stuck in staging.
1350     * 
1351     * @param car      The car to add to the list
1352     * @param buf      StringBuffer
1353     * @param carCount how many cars in the list
1354     */
1355    private void addCarToStuckStagingList(Car car, StringBuffer buf, int carCount) {
1356        if (carCount <= DISPLAY_CAR_LIMIT_20) {
1357            buf.append(NEW_LINE + " " + car.toString());
1358        } else if (carCount == DISPLAY_CAR_LIMIT_20 + 1) {
1359            buf.append(NEW_LINE +
1360                    MessageFormat.format(Bundle.getMessage("buildOnlyFirstXXXCars"),
1361                            new Object[] { DISPLAY_CAR_LIMIT_20, _departStageTrack.getName() }));
1362        }
1363    }
1364
1365    /**
1366     * Used to determine if a car on a staging track doesn't have a destination or
1367     * train
1368     * 
1369     * @return true if at least one car doesn't have a destination or train. false
1370     *         if all cars have a destination.
1371     */
1372    protected boolean isCarStuckStaging() {
1373        if (_departStageTrack == null) {
1374            return false;
1375        }
1376        // confirm that all cars in staging are departing
1377        for (Car car : _carList) {
1378            if (car.getTrack() == _departStageTrack &&
1379                    (car.getDestination() == null || car.getDestinationTrack() == null || car.getTrain() == null)) {
1380                return true;
1381            }
1382        }
1383        return false;
1384    }
1385
1386
1387
1388    /**
1389     * Add car to train, and adjust train length and weight
1390     *
1391     * @param car   the car being added to the train
1392     * @param rl    the departure route location for this car
1393     * @param rld   the destination route location for this car
1394     * @param track the destination track for this car
1395     *
1396     */
1397    protected void addCarToTrain(Car car, RouteLocation rl, RouteLocation rld, Track track) {
1398        addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildCarAssignedDest"),
1399                new Object[] { car.toString(), rld.getName(), track.getName() }));
1400        car.setDestination(track.getLocation(), track);
1401        int length = car.getTotalLength();
1402        int weightTons = car.getAdjustedWeightTons();
1403        // car could be part of a kernel
1404        if (car.getKernel() != null) {
1405            length = car.getKernel().getTotalLength(); // includes couplers
1406            weightTons = car.getKernel().getAdjustedWeightTons();
1407            List<Car> kCars = car.getKernel().getCars();
1408            addLine(_buildReport, THREE,
1409                    MessageFormat.format(Bundle.getMessage("buildCarPartOfKernel"),
1410                            new Object[] { car.toString(), car.getKernelName(), kCars.size(),
1411                                    car.getKernel().getTotalLength(), Setup.getLengthUnit().toLowerCase() }));
1412            for (Car kCar : kCars) {
1413                if (kCar != car) {
1414                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildCarKernelAssignedDest"),
1415                            new Object[] { kCar.toString(), kCar.getKernelName(), rld.getName(), track.getName() }));
1416                    kCar.setTrain(_train);
1417                    kCar.setRouteLocation(rl);
1418                    kCar.setRouteDestination(rld);
1419                    kCar.setDestination(track.getLocation(), track, true); // force destination
1420                    // save final destination and track values in case of train reset
1421                    kCar.setPreviousFinalDestination(car.getPreviousFinalDestination());
1422                    kCar.setPreviousFinalDestinationTrack(car.getPreviousFinalDestinationTrack());
1423                }
1424            }
1425            car.updateKernel();
1426        }
1427        // warn if car's load wasn't generated out of staging
1428        if (!_train.isLoadNameAccepted(car.getLoadName(), car.getTypeName())) {
1429            _warnings++;
1430            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildWarnCarDepartStaging"),
1431                    new Object[] { car.toString(), car.getLoadName() }));
1432        }
1433        addLine(_buildReport, THREE, BLANK_LINE);
1434        _numberCars++; // bump number of cars moved by this train
1435        _completedMoves++; // bump number of car pick up moves for the location
1436        _reqNumOfMoves--; // decrement number of moves left for the location
1437        if (_reqNumOfMoves <= 0) {
1438            _success = true; // done with this location!
1439        }
1440        _carList.remove(car);
1441        _carIndex--; // removed car from list, so backup pointer
1442
1443        rl.setCarMoves(rl.getCarMoves() + 1);
1444        if (rl != rld) {
1445            rld.setCarMoves(rld.getCarMoves() + 1);
1446        }
1447        // now adjust train length and weight for each location that car is in the train
1448        finishAddRsToTrain(car, rl, rld, length, weightTons);
1449    }
1450
1451    protected void finishAddRsToTrain(RollingStock rs, RouteLocation rl, RouteLocation rld, int length, int weightTons) {
1452        // notify that locations have been modified when build done
1453        // allows automation actions to run properly
1454        if (!_modifiedLocations.contains(rl.getLocation())) {
1455            _modifiedLocations.add(rl.getLocation());
1456        }
1457        if (!_modifiedLocations.contains(rld.getLocation())) {
1458            _modifiedLocations.add(rld.getLocation());
1459        }
1460        rs.setTrain(_train);
1461        rs.setRouteLocation(rl);
1462        rs.setRouteDestination(rld);
1463        // now adjust train length and weight for each location that the rolling stock
1464        // is in the train
1465        boolean inTrain = false;
1466        for (RouteLocation routeLocation : _routeList) {
1467            if (rl == routeLocation) {
1468                inTrain = true;
1469            }
1470            if (rld == routeLocation) {
1471                break;
1472            }
1473            if (inTrain) {
1474                routeLocation.setTrainLength(routeLocation.getTrainLength() + length); // includes couplers
1475                routeLocation.setTrainWeight(routeLocation.getTrainWeight() + weightTons);
1476            }
1477        }
1478    }
1479
1480    /**
1481     * Determine if rolling stock can be picked up based on train direction at the
1482     * route location.
1483     * 
1484     * @param rs The rolling stock
1485     * @param rl The rolling stock's route location
1486     * @throws BuildFailedException if coding issue
1487     * @return true if there isn't a problem
1488     */
1489    protected boolean checkPickUpTrainDirection(RollingStock rs, RouteLocation rl) throws BuildFailedException {
1490        // Code Check, car or engine should have a track assignment
1491        if (rs.getTrack() == null) {
1492            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildWarningRsNoTrack"),
1493                    new Object[] { rs.toString(), rs.getLocationName() }));
1494        }
1495        // ignore local switcher direction
1496        if (_train.isLocalSwitcher()) {
1497            return true;
1498        }
1499        if ((rl.getTrainDirection() &
1500                rs.getLocation().getTrainDirections() &
1501                rs.getTrack().getTrainDirections()) != 0) {
1502            return true;
1503        }
1504
1505        // Only track direction can cause the following message. Location direction has
1506        // already been checked
1507        addLine(_buildReport, SEVEN,
1508                MessageFormat.format(Bundle.getMessage("buildRsCanNotPickupUsingTrain"), new Object[] { rs.toString(),
1509                        rl.getTrainDirectionString(), rs.getTrackName(), rs.getLocationName(), rl.getId() }));
1510        return false;
1511    }
1512
1513    /**
1514     * Used to report a problem picking up the rolling stock due to train direction.
1515     * 
1516     * @param rl The route location
1517     * @return true if there isn't a problem
1518     */
1519    protected boolean checkPickUpTrainDirection(RouteLocation rl) {
1520        // ignore local switcher direction
1521        if (_train.isLocalSwitcher()) {
1522            return true;
1523        }
1524        if ((rl.getTrainDirection() & rl.getLocation().getTrainDirections()) != 0) {
1525            return true;
1526        }
1527
1528        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildLocDirection"),
1529                new Object[] { rl.getName(), rl.getTrainDirectionString() }));
1530        return false;
1531    }
1532
1533    /**
1534     * Checks to see if train length would be exceeded if this car was added to the
1535     * train.
1536     *
1537     * @param car the car in question
1538     * @param rl  the departure route location for this car
1539     * @param rld the destination route location for this car
1540     * @return true if car can be added to train
1541     */
1542    protected boolean checkTrainLength(Car car, RouteLocation rl, RouteLocation rld) {
1543        // car can be a kernel so get total length
1544        int length = car.getTotalLength();
1545        if (car.getKernel() != null) {
1546            length = car.getKernel().getTotalLength();
1547        }
1548        boolean carInTrain = false;
1549        for (RouteLocation rlt : _routeList) {
1550            if (rl == rlt) {
1551                carInTrain = true;
1552            }
1553            if (rld == rlt) {
1554                break;
1555            }
1556            if (carInTrain && rlt.getTrainLength() + length > rlt.getMaxTrainLength()) {
1557                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildCanNotPickupCarLength"),
1558                        new Object[] { car.toString(), length, Setup.getLengthUnit().toLowerCase(),
1559                                rlt.getMaxTrainLength(), Setup.getLengthUnit().toLowerCase(),
1560                                rlt.getTrainLength() + length - rlt.getMaxTrainLength(), rlt.getName(), rlt.getId() }));
1561                return false;
1562            }
1563        }
1564        return true;
1565    }
1566
1567    // TODO future option to ignore train direction at last location in train's
1568    // route
1569//    private final boolean ignoreTrainDirectionIfLastLoc = false;
1570
1571    // FIXME: ignoreTrainDirectionIfLastLoc has no way to become true, hence the if
1572    // statement using it below cannot ever be true
1573    protected boolean checkDropTrainDirection(RollingStock rs, RouteLocation rld, Track track) {
1574        // local?
1575        if (_train.isLocalSwitcher()) {
1576            return true;
1577        }
1578        // is the destination the last location on the route?
1579//        if (ignoreTrainDirectionIfLastLoc && rld == _train.getTrainTerminatesRouteLocation()) {
1580//            return true; // yes, ignore train direction
1581//        }
1582        // this location only services trains with these directions
1583        int serviceTrainDir = rld.getLocation().getTrainDirections();
1584        if (track != null) {
1585            serviceTrainDir = serviceTrainDir & track.getTrainDirections();
1586        }
1587
1588        // is this a car going to alternate track? Check to see if direct move from
1589        // alternate to FD track is possible
1590        if ((rld.getTrainDirection() & serviceTrainDir) != 0 &&
1591                rs != null &&
1592                track != null &&
1593                Car.class.isInstance(rs)) {
1594            Car car = (Car) rs;
1595            if (car.getFinalDestinationTrack() != null &&
1596                    track == car.getFinalDestinationTrack().getAlternateTrack() &&
1597                    (track.getTrainDirections() & car.getFinalDestinationTrack().getTrainDirections()) == 0) {
1598                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropRsUsingTrain4"),
1599                        new Object[] { car.getFinalDestinationTrack().getName(),
1600                                formatStringToCommaSeparated(
1601                                        Setup.getDirectionStrings(car.getFinalDestinationTrack().getTrainDirections())),
1602                                car.getFinalDestinationTrack().getAlternateTrack().getName(),
1603                                formatStringToCommaSeparated(Setup.getDirectionStrings(
1604                                        car.getFinalDestinationTrack().getAlternateTrack().getTrainDirections())) }));
1605                return false;
1606            }
1607        }
1608
1609        if ((rld.getTrainDirection() & serviceTrainDir) != 0) {
1610            return true;
1611        }
1612        if (rs == null || track == null) {
1613            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildDestinationDoesNotService"),
1614                    new Object[] { rld.getName(), rld.getTrainDirectionString() }));
1615        } else {
1616            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropRsUsingTrain"),
1617                    new Object[] { rs.toString(), rld.getTrainDirectionString(), track.getName() }));
1618        }
1619        return false;
1620    }
1621
1622    protected boolean checkDropTrainDirection(RouteLocation rld) {
1623        return (checkDropTrainDirection(null, rld, null));
1624    }
1625
1626    /**
1627     * Determinate if car can be dropped by this train to the track specified.
1628     *
1629     * @param car   the car.
1630     * @param track the destination track.
1631     * @return true if able to drop.
1632     */
1633    protected boolean checkTrainCanDrop(Car car, Track track) {
1634        if (track.isInterchange() || track.isSpur()) {
1635            if (track.getDropOption().equals(Track.TRAINS) || track.getDropOption().equals(Track.EXCLUDE_TRAINS)) {
1636                if (track.isDropTrainAccepted(_train)) {
1637                    log.debug("Car ({}) can be droped by train to track ({})", car.toString(), track.getName());
1638                } else {
1639                    addLine(_buildReport, SEVEN,
1640                            MessageFormat.format(Bundle.getMessage("buildCanNotDropCarTrain"), new Object[]{
1641                                    car.toString(), _train.getName(), track.getTrackTypeName(),
1642                                    track.getLocation().getName(), track.getName()}));
1643                    return false;
1644                }
1645            }
1646            if (track.getDropOption().equals(Track.ROUTES) || track.getDropOption().equals(Track.EXCLUDE_ROUTES)) {
1647                if (track.isDropRouteAccepted(_train.getRoute())) {
1648                    log.debug("Car ({}) can be droped by route to track ({})", car.toString(), track.getName());
1649                } else {
1650                    addLine(_buildReport, SEVEN,
1651                            MessageFormat.format(Bundle.getMessage("buildCanNotDropCarRoute"),
1652                                    new Object[]{car.toString(), _train.getRoute().getName(),
1653                                            track.getTrackTypeName(), track.getLocation().getName(), track.getName()}));
1654                    return false;
1655                }
1656            }
1657        }
1658        return true;
1659    }
1660
1661    /**
1662     * Check departure staging track to see if engines and cars are available to a
1663     * new train. Also confirms that the engine and car type, load, road, etc. are
1664     * accepted by the train.
1665     * 
1666     * @param departStageTrack The staging track
1667     *
1668     * @return true is there are engines and cars available.
1669     */
1670    protected boolean checkDepartureStagingTrack(Track departStageTrack) {
1671        // does this staging track service this train?
1672        if (!departStageTrack.isPickupTrainAccepted(_train)) {
1673            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingNotTrain"),
1674                    new Object[] { departStageTrack.getName() }));
1675            return false;
1676        }
1677        if (departStageTrack.getNumberRS() == 0 && _train.getTrainDepartsRouteLocation().getMaxCarMoves() > 0) {
1678            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingEmpty"),
1679                    new Object[] { departStageTrack.getName() }));
1680            return false;
1681        }
1682        if (departStageTrack.getUsedLength() > _train.getTrainDepartsRouteLocation().getMaxTrainLength()) {
1683            addLine(_buildReport, THREE,
1684                    MessageFormat.format(Bundle.getMessage("buildStagingTrainTooLong"),
1685                            new Object[] { departStageTrack.getName(), departStageTrack.getUsedLength(),
1686                                    Setup.getLengthUnit().toLowerCase(),
1687                                    _train.getTrainDepartsRouteLocation().getMaxTrainLength() }));
1688            return false;
1689        }
1690        if (departStageTrack.getNumberCars() > _train.getTrainDepartsRouteLocation().getMaxCarMoves()) {
1691            addLine(_buildReport, THREE,
1692                    MessageFormat.format(Bundle.getMessage("buildStagingTooManyCars"),
1693                            new Object[] { departStageTrack.getName(), departStageTrack.getNumberCars(),
1694                                    _train.getTrainDepartsRouteLocation().getMaxCarMoves() }));
1695            return false;
1696        }
1697        // does the staging track have the right number of locomotives?
1698        if (!_train.getNumberEngines().equals("0") &&
1699                getNumberEngines(_train.getNumberEngines()) != departStageTrack.getNumberEngines()) {
1700            addLine(_buildReport, THREE,
1701                    MessageFormat.format(Bundle.getMessage("buildStagingNotEngines"),
1702                            new Object[] { departStageTrack.getName(), departStageTrack.getNumberEngines(),
1703                                    _train.getNumberEngines() }));
1704            return false;
1705        }
1706        // is the staging track direction correct for this train?
1707        if ((departStageTrack.getTrainDirections() & _train.getTrainDepartsRouteLocation().getTrainDirection()) == 0) {
1708            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingNotDirection"),
1709                    new Object[] { departStageTrack.getName() }));
1710            return false;
1711        }
1712
1713        if (departStageTrack.getNumberEngines() > 0) {
1714            for (Engine eng : engineManager.getList()) {
1715                if (eng.getTrack() == departStageTrack) {
1716                    // has engine been assigned to another train?
1717                    if (eng.getRouteLocation() != null) {
1718                        addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingDepart"),
1719                                new Object[] { departStageTrack.getName(), eng.getTrainName() }));
1720                        return false;
1721                    }
1722                    if (eng.getTrain() != null && eng.getTrain() != _train) {
1723                        addLine(_buildReport, THREE, MessageFormat.format(
1724                                Bundle.getMessage("buildStagingDepartEngineTrain"),
1725                                new Object[] { departStageTrack.getName(), eng.toString(), eng.getTrainName() }));
1726                        return false;
1727                    }
1728                    // does the train accept the engine type from the staging track?
1729                    if (!_train.isTypeNameAccepted(eng.getTypeName())) {
1730                        addLine(_buildReport, THREE,
1731                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineType"),
1732                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getTypeName(),
1733                                                _train.getName() }));
1734                        return false;
1735                    }
1736                    // does the train accept the engine model from the staging track?
1737                    if (!_train.getEngineModel().equals(Train.NONE) &&
1738                            !_train.getEngineModel().equals(eng.getModel())) {
1739                        addLine(_buildReport, THREE,
1740                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineModel"),
1741                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getModel(),
1742                                                _train.getName() }));
1743                        return false;
1744                    }
1745                    // does the engine road match the train requirements?
1746                    if (!_train.getCarRoadOption().equals(Train.ALL_ROADS) &&
1747                            !_train.getEngineRoad().equals(Train.NONE) &&
1748                            !_train.getEngineRoad().equals(eng.getRoadName())) {
1749                        addLine(_buildReport, THREE,
1750                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineRoad"),
1751                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getRoadName(),
1752                                                _train.getName() }));
1753                        return false;
1754                    }
1755                    // does the train accept the engine road from the staging track?
1756                    if (_train.getEngineRoad().equals(Train.NONE) && !_train.isLocoRoadNameAccepted(eng.getRoadName())) {
1757                        addLine(_buildReport, THREE,
1758                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineRoad"),
1759                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getRoadName(),
1760                                                _train.getName() }));
1761                        return false;
1762                    }
1763                    // does the train accept the engine owner from the staging track?
1764                    if (!_train.isOwnerNameAccepted(eng.getOwnerName())) {
1765                        addLine(_buildReport, THREE,
1766                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineOwner"),
1767                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getOwnerName(),
1768                                                _train.getName() }));
1769                        return false;
1770                    }
1771                    // does the train accept the engine built date from the staging track?
1772                    if (!_train.isBuiltDateAccepted(eng.getBuilt())) {
1773                        addLine(_buildReport, THREE,
1774                                MessageFormat.format(Bundle.getMessage("buildStagingDepartEngineBuilt"),
1775                                        new Object[] { departStageTrack.getName(), eng.toString(), eng.getBuilt(),
1776                                                _train.getName() }));
1777                        return false;
1778                    }
1779                }
1780            }
1781        }
1782        boolean foundCaboose = false;
1783        boolean foundFRED = false;
1784        if (departStageTrack.getNumberCars() > 0) {
1785            for (Car car : carManager.getList()) {
1786                if (car.getTrack() != departStageTrack) {
1787                    continue;
1788                }
1789                // ignore non-lead cars in kernels
1790                if (car.getKernel() != null && !car.isLead()) {
1791                    continue; // ignore non-lead cars
1792                }
1793                // has car been assigned to another train?
1794                if (car.getRouteLocation() != null) {
1795                    log.debug("Car ({}) has route location ({})", car.toString(), car.getRouteLocation().getName());
1796                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingDepart"),
1797                            new Object[] { departStageTrack.getName(), car.getTrainName() }));
1798                    return false;
1799                }
1800                if (car.getTrain() != null && car.getTrain() != _train) {
1801                    addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingDepartCarTrain"),
1802                            new Object[] { departStageTrack.getName(), car.toString(), car.getTrainName() }));
1803                    return false;
1804                }
1805                // does the train accept the car type from the staging track?
1806                if (!_train.isTypeNameAccepted(car.getTypeName())) {
1807                    addLine(_buildReport, THREE,
1808                            MessageFormat.format(Bundle.getMessage("buildStagingDepartCarType"), new Object[] {
1809                                    departStageTrack.getName(), car.toString(), car.getTypeName(), _train.getName() }));
1810                    return false;
1811                }
1812                // does the train accept the car road from the staging track?
1813                if (!_train.isCarRoadNameAccepted(car.getRoadName())) {
1814                    addLine(_buildReport, THREE,
1815                            MessageFormat.format(Bundle.getMessage("buildStagingDepartCarRoad"), new Object[] {
1816                                    departStageTrack.getName(), car.toString(), car.getRoadName(), _train.getName() }));
1817                    return false;
1818                }
1819                // does the train accept the car load from the staging track?
1820                if (!car.isCaboose() &&
1821                        !car.isPassenger() &&
1822                        (!car.getLoadName().equals(carLoads.getDefaultEmptyName()) ||
1823                                !departStageTrack.isAddCustomLoadsEnabled() &&
1824                                        !departStageTrack.isAddCustomLoadsAnySpurEnabled() &&
1825                                        !departStageTrack.isAddCustomLoadsAnyStagingTrackEnabled()) &&
1826                        !_train.isLoadNameAccepted(car.getLoadName(), car.getTypeName())) {
1827                    addLine(_buildReport, THREE,
1828                            MessageFormat.format(Bundle.getMessage("buildStagingDepartCarLoad"), new Object[] {
1829                                    departStageTrack.getName(), car.toString(), car.getLoadName(), _train.getName() }));
1830                    return false;
1831                }
1832                // does the train accept the car owner from the staging track?
1833                if (!_train.isOwnerNameAccepted(car.getOwnerName())) {
1834                    addLine(_buildReport, THREE,
1835                            MessageFormat.format(Bundle.getMessage("buildStagingDepartCarOwner"), new Object[] {
1836                                    departStageTrack.getName(), car.toString(), car.getOwnerName(), _train.getName() }));
1837                    return false;
1838                }
1839                // does the train accept the car built date from the staging track?
1840                if (!_train.isBuiltDateAccepted(car.getBuilt())) {
1841                    addLine(_buildReport, THREE,
1842                            MessageFormat.format(Bundle.getMessage("buildStagingDepartCarBuilt"), new Object[] {
1843                                    departStageTrack.getName(), car.toString(), car.getBuilt(), _train.getName() }));
1844                    return false;
1845                }
1846                // does the car have a destination serviced by this train?
1847                if (car.getDestination() != null) {
1848                    log.debug("Car ({}) has a destination ({}, {})", car.toString(), car.getDestinationName(),
1849                            car.getDestinationTrackName());
1850                    if (!_train.isServiceable(car)) {
1851                        addLine(_buildReport, THREE,
1852                                MessageFormat.format(Bundle.getMessage("buildStagingDepartCarDestination"),
1853                                        new Object[] { departStageTrack.getName(), car.toString(),
1854                                                car.getDestinationName(), _train.getName() }));
1855                        return false;
1856                    }
1857                }
1858                // is this car a caboose with the correct road for this train?
1859                if (car.isCaboose() &&
1860                        (_train.getCabooseRoad().equals(Train.NONE) ||
1861                                _train.getCabooseRoad().equals(car.getRoadName()))) {
1862                    foundCaboose = true;
1863                }
1864                // is this car have a FRED with the correct road for this train?
1865                if (car.hasFred() &&
1866                        (_train.getCabooseRoad().equals(Train.NONE) ||
1867                                _train.getCabooseRoad().equals(car.getRoadName()))) {
1868                    foundFRED = true;
1869                }
1870            }
1871        }
1872        // does the train require a caboose and did we find one from staging?
1873        if (_train.isCabooseNeeded() && !foundCaboose) {
1874            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingNoCaboose"),
1875                    new Object[] { departStageTrack.getName(), _train.getCabooseRoad() }));
1876            return false;
1877        }
1878        // does the train require a car with FRED and did we find one from staging?
1879        if (_train.isFredNeeded() && !foundFRED) {
1880            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildStagingNoCarFRED"),
1881                    new Object[] { departStageTrack.getName(), _train.getCabooseRoad() }));
1882            return false;
1883        }
1884        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainCanDepartTrack"),
1885                new Object[] { _train.getName(), departStageTrack.getName() }));
1886        return true;
1887    }
1888
1889    /**
1890     * Checks to see if staging track can accept train.
1891     * 
1892     * @param terminateStageTrack the staging track
1893     *
1894     * @return true if staging track is empty, not reserved, and accepts car and
1895     *         engine types, roads, and loads.
1896     */
1897    protected boolean checkTerminateStagingTrack(Track terminateStageTrack) {
1898        if (!terminateStageTrack.isDropTrainAccepted(_train)) {
1899            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingNotTrain"),
1900                    new Object[] { terminateStageTrack.getName() }));
1901            return false;
1902        }
1903        // In normal mode, find a completely empty track. In aggressive mode, a track
1904        // that scheduled to depart is okay
1905        if (((!Setup.isBuildAggressive() || !Setup.isStagingTrackImmediatelyAvail()) &&
1906                terminateStageTrack.getNumberRS() != 0) ||
1907                terminateStageTrack.getNumberRS() != terminateStageTrack.getPickupRS()) {
1908            addLine(_buildReport, FIVE,
1909                    MessageFormat.format(Bundle.getMessage("buildStagingTrackOccupied"),
1910                            new Object[] { terminateStageTrack.getName(), terminateStageTrack.getNumberEngines(),
1911                                    terminateStageTrack.getNumberCars() }));
1912            return false;
1913        }
1914        if (terminateStageTrack.getDropRS() != 0) {
1915            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackReserved"),
1916                    new Object[] { terminateStageTrack.getName(), terminateStageTrack.getDropRS() }));
1917            return false;
1918        }
1919        if (terminateStageTrack.getPickupRS() > 0) {
1920            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackDepart"),
1921                    new Object[] { terminateStageTrack.getName() }));
1922        }
1923        // if track is setup to accept a specific train or route, then ignore other
1924        // track restrictions
1925        if (terminateStageTrack.getDropOption().equals(Track.TRAINS) ||
1926                terminateStageTrack.getDropOption().equals(Track.ROUTES)) {
1927            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainCanTerminateTrack"),
1928                    new Object[] { _train.getName(), terminateStageTrack.getName() }));
1929            return true; // train can drop to this track, ignore other track restrictions
1930        }
1931        if (!Setup.isStagingTrainCheckEnabled()) {
1932            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainCanTerminateTrack"),
1933                    new Object[] { _train.getName(), terminateStageTrack.getName() }));
1934            return true;
1935        } else if (!checkTerminateStagingTrackRestrictions(terminateStageTrack)) {
1936            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildStagingTrackRestriction"),
1937                    new Object[] { terminateStageTrack.getName(), _train.getName() }));
1938            addLine(_buildReport, SEVEN, Bundle.getMessage("buildOptionRestrictStaging"));
1939            return false;
1940        }
1941        return true;
1942    }
1943
1944    private boolean checkTerminateStagingTrackRestrictions(Track terminateStageTrack) {
1945        // check go see if location/track will accept the train's car and engine types
1946        for (String name : _train.getTypeNames()) {
1947            if (!_terminateLocation.acceptsTypeName(name)) {
1948                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildDestinationType"),
1949                        new Object[] { _terminateLocation.getName(), name }));
1950                return false;
1951            }
1952            if (!terminateStageTrack.isTypeNameAccepted(name)) {
1953                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackType"),
1954                        new Object[] { terminateStageTrack.getName(), name }));
1955                return false;
1956            }
1957        }
1958        // check go see if track will accept the train's car roads
1959        if (_train.getCarRoadOption().equals(Train.ALL_ROADS) &&
1960                !terminateStageTrack.getRoadOption().equals(Track.ALL_ROADS)) {
1961            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackAllRoads"),
1962                    new Object[] { terminateStageTrack.getName() }));
1963            return false;
1964        }
1965        // now determine if roads accepted by train are also accepted by staging track
1966        // TODO should we be checking loco road names?
1967        for (String road : InstanceManager.getDefault(CarRoads.class).getNames()) {
1968            if (_train.isCarRoadNameAccepted(road)) {
1969                if (!terminateStageTrack.isRoadNameAccepted(road)) {
1970                    addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackRoad"),
1971                            new Object[] { terminateStageTrack.getName(), road }));
1972                    return false;
1973                }
1974            }
1975        }
1976
1977        // determine if staging will accept loads carried by train
1978        if (_train.getLoadOption().equals(Train.ALL_LOADS) &&
1979                !terminateStageTrack.getLoadOption().equals(Track.ALL_LOADS)) {
1980            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackAllLoads"),
1981                    new Object[] { terminateStageTrack.getName() }));
1982            return false;
1983        }
1984        // get all of the types and loads that a train can carry, and determine if
1985        // staging will accept
1986        for (String type : _train.getTypeNames()) {
1987            for (String load : carLoads.getNames(type)) {
1988                if (_train.isLoadNameAccepted(load, type)) {
1989                    if (!terminateStageTrack.isLoadNameAndCarTypeAccepted(load, type)) {
1990                        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildStagingTrackLoad"),
1991                                new Object[] { terminateStageTrack.getName(), type + CarLoad.SPLIT_CHAR + load }));
1992                        return false;
1993                    }
1994                }
1995            }
1996        }
1997        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainCanTerminateTrack"),
1998                new Object[] { _train.getName(), terminateStageTrack.getName() }));
1999        return true;
2000    }
2001
2002    boolean routeToTrackFound;
2003
2004    protected boolean checkBasicMoves(Car car, Track track) {
2005        if (car.getTrack() == track) {
2006            return false;
2007        }
2008        // don't allow local move to track with a "similar" name
2009        if (splitString(car.getLocationName()).equals(splitString(track.getLocation().getName())) &&
2010                splitString(car.getTrackName()).equals(splitString(track.getName()))) {
2011            return false;
2012        }
2013        if (track.isStaging() && car.getLocation() == track.getLocation()) {
2014            return false; // don't use same staging location
2015        }
2016        // is the car's destination the terminal and is that allowed?
2017        if (!checkThroughCarsAllowed(car, track.getLocation().getName())) {
2018            return false;
2019        }
2020        if (!checkLocalMovesAllowed(car, track)) {
2021            return false;
2022        }
2023        return true;
2024    }
2025
2026    /**
2027     * Used when generating a car load from staging.
2028     *
2029     * @param car   the car.
2030     * @param track the car's destination track that has the schedule.
2031     * @return ScheduleItem si if match found, null otherwise.
2032     * @throws BuildFailedException if schedule doesn't have any line items
2033     */
2034    protected ScheduleItem getScheduleItem(Car car, Track track) throws BuildFailedException {
2035        if (track.getSchedule() == null) {
2036            return null;
2037        }
2038        if (!track.isTypeNameAccepted(car.getTypeName())) {
2039            log.debug("Track ({}) doesn't service car type ({})", track.getName(), car.getTypeName());
2040            if (!Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_NORMAL)) {
2041                addLine(_buildReport, SEVEN,
2042                        MessageFormat.format(Bundle.getMessage("buildSpurNotThisType"),
2043                                new Object[] { track.getLocation().getName(), track.getName(), track.getScheduleName(),
2044                                        car.getTypeName() }));
2045            }
2046            return null;
2047        }
2048        ScheduleItem si = null;
2049        if (track.getScheduleMode() == Track.SEQUENTIAL) {
2050            si = track.getCurrentScheduleItem();
2051            // code check
2052            if (si == null) {
2053                throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorNoScheduleItem"),
2054                        new Object[] { track.getScheduleItemId(), track.getScheduleName(), track.getName(),
2055                                track.getLocation().getName() }));
2056            }
2057            return checkScheduleItem(si, car, track);
2058        }
2059        log.debug("Track ({}) in match mode", track.getName());
2060        // go through entire schedule looking for a match
2061        for (int i = 0; i < track.getSchedule().getSize(); i++) {
2062            si = track.getNextScheduleItem();
2063            // code check
2064            if (si == null) {
2065                throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorNoScheduleItem"),
2066                        new Object[] { track.getScheduleItemId(), track.getScheduleName(), track.getName(),
2067                                track.getLocation().getName() }));
2068            }
2069            si = checkScheduleItem(si, car, track);
2070            if (si != null) {
2071                break;
2072            }
2073        }
2074        return si;
2075    }
2076
2077    /**
2078     * Used when generating a car load from staging.
2079     * 
2080     * Checks a schedule item to see if the car type matches, and the train and
2081     * track can service the schedule item's load. This code doesn't check to see if
2082     * the car's load can be serviced by the schedule. Instead a schedule item is
2083     * returned that allows the program to assign a custom load to the car that
2084     * matches a schedule item. Therefore, schedule items that don't request a
2085     * custom load are ignored.
2086     *
2087     * @param si    the schedule item
2088     * @param car   the car to check
2089     * @param track the destination track
2090     * @return Schedule item si if okay, null otherwise.
2091     */
2092    private ScheduleItem checkScheduleItem(ScheduleItem si, Car car, Track track) {
2093        if (!car.getTypeName().equals(si.getTypeName()) ||
2094                si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
2095                si.getReceiveLoadName().equals(carLoads.getDefaultEmptyName()) ||
2096                si.getReceiveLoadName().equals(carLoads.getDefaultLoadName())) {
2097            log.debug("Not using track ({}) schedule request type ({}) road ({}) load ({})", track.getName(),
2098                    si.getTypeName(), si.getRoadName(), si.getReceiveLoadName()); // NOI18N
2099            if (!Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_NORMAL)) {
2100                addLine(_buildReport, SEVEN,
2101                        MessageFormat.format(Bundle.getMessage("buildSpurScheduleNotUsed"),
2102                                new Object[] { track.getLocation().getName(), track.getName(), track.getScheduleName(),
2103                                        si.getId(), track.getScheduleModeName().toLowerCase(), si.getTypeName(),
2104                                        si.getRoadName(), si.getReceiveLoadName() }));
2105            }
2106            return null;
2107        }
2108        if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) {
2109            log.debug("Not using track ({}) schedule request type ({}) road ({}) load ({})", track.getName(),
2110                    si.getTypeName(), si.getRoadName(), si.getReceiveLoadName()); // NOI18N
2111            if (!Setup.getRouterBuildReportLevel().equals(Setup.BUILD_REPORT_NORMAL)) {
2112                addLine(_buildReport, SEVEN,
2113                        MessageFormat.format(Bundle.getMessage("buildSpurScheduleNotUsed"),
2114                                new Object[] { track.getLocation().getName(), track.getName(), track.getScheduleName(),
2115                                        si.getId(), track.getScheduleModeName().toLowerCase(), si.getTypeName(),
2116                                        si.getRoadName(), si.getReceiveLoadName() }));
2117            }
2118            return null;
2119        }
2120        if (!_train.isLoadNameAccepted(si.getReceiveLoadName(), si.getTypeName())) {
2121            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrainNotNewLoad"), new Object[] {
2122                    _train.getName(), si.getReceiveLoadName(), track.getLocation().getName(), track.getName() }));
2123            return null;
2124        }
2125        // does the departure track allow this load?
2126        if (!car.getTrack().isLoadNameAndCarTypeShipped(si.getReceiveLoadName(), car.getTypeName())) {
2127            addLine(_buildReport, SEVEN,
2128                    MessageFormat.format(Bundle.getMessage("buildTrackNotLoadSchedule"),
2129                            new Object[] { car.getTrackName(), si.getReceiveLoadName(), track.getLocation().getName(),
2130                                    track.getName(), si.getId() }));
2131            return null;
2132        }
2133        if (!si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) &&
2134                !trainScheduleManager.getTrainScheduleActiveId().equals(si.getSetoutTrainScheduleId())) {
2135            log.debug("Schedule item isn't active");
2136            // build the status message
2137            TrainSchedule aSch = trainScheduleManager.getScheduleById(trainScheduleManager.getTrainScheduleActiveId());
2138            TrainSchedule tSch = trainScheduleManager.getScheduleById(si.getSetoutTrainScheduleId());
2139            String aName = "";
2140            String tName = "";
2141            if (aSch != null) {
2142                aName = aSch.getName();
2143            }
2144            if (tSch != null) {
2145                tName = tSch.getName();
2146            }
2147            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildScheduleNotActive"),
2148                    new Object[] { track.getName(), si.getId(), tName, aName }));
2149
2150            return null;
2151        }
2152        if (!si.getRandom().equals(ScheduleItem.NONE)) {
2153            try {
2154                int value = Integer.parseInt(si.getRandom());
2155                double random = 100 * Math.random();
2156                log.debug("Selected random {}, created random {}", si.getRandom(), random);
2157                if (random > value) {
2158                    addLine(_buildReport, SEVEN,
2159                            MessageFormat.format(Bundle.getMessage("buildScheduleRandom"),
2160                                    new Object[] { track.getLocation().getName(), track.getName(),
2161                                            track.getScheduleName(), si.getId(), si.getReceiveLoadName(), value,
2162                                            random }));
2163                    return null;
2164                }
2165            } catch (NumberFormatException e) {
2166                log.error("Schedule item ({}) random value ({}) isn't a number", si.getId(), si.getRandom());
2167            }
2168        }
2169        log.debug("Found track ({}) schedule item id ({}) for car ({})", track.getName(), si.getId(), car.toString());
2170        return si;
2171    }
2172
2173    protected void showCarServiceOrder(Car car) {
2174        if (!car.getTrack().getServiceOrder().equals(Track.NORMAL)) {
2175            addLine(_buildReport, SEVEN,
2176                    MessageFormat.format(Bundle.getMessage("buildTrackModePriority"),
2177                            new Object[] { car.toString(), car.getTrack().getTrackTypeName(), car.getTrackName(),
2178                                    car.getTrack().getServiceOrder(), car.getLastDate() }));
2179        }
2180    }
2181
2182    /**
2183     * Returns a list containing two tracks. The 1st track found for the car, the
2184     * 2nd track is the car's final destination if an alternate track was used for
2185     * the car. 2nd track can be null.
2186     * 
2187     * @param car The car needing a destination track
2188     * @param rld the RouteLocation destination
2189     * @return List containing up to two tracks. No tracks if none found.
2190     */
2191    protected List<Track> getTracksAtDestination(Car car, RouteLocation rld) {
2192        List<Track> tracks = new ArrayList<>();
2193        Location testDestination = rld.getLocation();
2194        // first report if there are any alternate tracks
2195        for (Track track : testDestination.getTracksByNameList(null)) {
2196            if (track.isAlternate()) {
2197                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrackIsAlternate"),
2198                        new Object[] { car.toString(), track.getTrackTypeName(), track.getName() }));
2199            }
2200        }
2201        // now find a track for this car
2202        for (Track testTrack : testDestination.getTracksByMoves(null)) {
2203            // normally don't move car to a track with the same name at the same location
2204            if (splitString(car.getLocationName()).equals(splitString(testTrack.getLocation().getName())) &&
2205                    splitString(car.getTrackName()).equals(splitString(testTrack.getName())) &&
2206                    !car.isPassenger() &&
2207                    !car.isCaboose() &&
2208                    !car.hasFred()) {
2209                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropCarSameTrack"),
2210                        new Object[] { car.toString(), testTrack.getName() }));
2211                continue;
2212            }
2213            // Can the train service this track?
2214            if (!checkDropTrainDirection(car, rld, testTrack)) {
2215                continue;
2216            }
2217            // drop to interchange or spur?
2218            if (!checkTrainCanDrop(car, testTrack)) {
2219                continue;
2220            }
2221            // report if track has planned pickups
2222            if (testTrack.getIgnoreUsedLengthPercentage() > 0) {
2223                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildTrackHasPlannedPickups"),
2224                        new Object[] { testTrack.getName(), testTrack.getIgnoreUsedLengthPercentage(),
2225                                testTrack.getLength(), Setup.getLengthUnit().toLowerCase(), testTrack.getUsedLength(),
2226                                testTrack.getReserved(), testTrack.getReservedLengthDrops(),
2227                                testTrack.getReservedLengthDrops() - testTrack.getReserved(),
2228                                testTrack.getAvailableTrackSpace() }));
2229            }
2230            String status = car.checkDestination(testDestination, testTrack);
2231            // Can be a caboose or car with FRED with a custom load
2232            // is the destination a spur with a schedule demanding this car's custom load?
2233            if (status.equals(Track.OKAY) &&
2234                    !testTrack.getScheduleId().equals(Track.NONE) &&
2235                    !car.getLoadName().equals(carLoads.getDefaultEmptyName()) &&
2236                    !car.getLoadName().equals(carLoads.getDefaultLoadName())) {
2237                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildSpurScheduleLoad"),
2238                        new Object[] { testTrack.getName(), car.getLoadName() }));
2239            }
2240            // check to see if alternate track is available if track full
2241            if (status.startsWith(Track.LENGTH) &&
2242                    testTrack.getAlternateTrack() != null &&
2243                    car.getTrack() != testTrack.getAlternateTrack() &&
2244                    checkTrainCanDrop(car, testTrack.getAlternateTrack())) {
2245                addLine(_buildReport, SEVEN,
2246                        MessageFormat.format(Bundle.getMessage("buildTrackFullHasAlternate"),
2247                                new Object[] { testDestination.getName(), testTrack.getName(),
2248                                        testTrack.getAlternateTrack().getName() }));
2249                status = car.checkDestination(testDestination, testTrack.getAlternateTrack());
2250                if (!status.equals(Track.OKAY)) {
2251                    addLine(_buildReport, SEVEN,
2252                            MessageFormat.format(Bundle.getMessage("buildCanNotDropCarBecause"),
2253                                    new Object[] { car.toString(), testTrack.getAlternateTrack().getName(), status,
2254                                            testTrack.getAlternateTrack().getTrackTypeName() }));
2255                    continue;
2256                }
2257                tracks.add(testTrack.getAlternateTrack()); // send car to alternate track
2258                tracks.add(testTrack); // car's final destination
2259                break; // done with this destination
2260            }
2261            // okay to drop car?
2262            if (!status.equals(Track.OKAY)) {
2263                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropCarBecause"),
2264                        new Object[] { car.toString(), testTrack.getName(), status, testTrack.getTrackTypeName() }));
2265                continue;
2266            }
2267            if (!checkForLocalMove(car, testTrack)) {
2268                continue;
2269            }
2270            tracks.add(testTrack);
2271            tracks.add(null); // no final destination for this car
2272            break; // done with this destination
2273        }
2274        return tracks;
2275    }
2276
2277    /**
2278     * Used to determine if car could be set out at earlier location in the train's
2279     * route.
2280     * 
2281     * @param car       The car
2282     * @param trackTemp The destination track for this car
2283     * @param rld       Where in the route the destination track was found
2284     * @param start     Where to begin the check
2285     * @param routeEnd  Where to stop the check
2286     * @return The best RouteLocation to drop off the car
2287     */
2288    protected RouteLocation checkForEarlierDrop(Car car, Track trackTemp, RouteLocation rld, int start, int routeEnd) {
2289        for (int m = start; m < routeEnd; m++) {
2290            RouteLocation rle = _routeList.get(m);
2291            if (rle == rld) {
2292                break;
2293            }
2294            if (rle.getName().equals(rld.getName()) &&
2295                    (rle.getCarMoves() < rle.getMaxCarMoves()) &&
2296                    rle.isDropAllowed() &&
2297                    checkDropTrainDirection(car, rle, trackTemp)) {
2298                log.debug("Found an earlier drop for car ({}) destination ({})", car.toString(), rle.getName()); // NOI18N
2299                return rle; // earlier drop in train's route
2300            }
2301        }
2302        return rld;
2303    }
2304
2305    /**
2306     * Checks to see if local move is allowed for this car
2307     * 
2308     * @param car       the car being moved
2309     * @param testTrack the destination track for this car
2310     * @return false if local move not allowed
2311     */
2312    private boolean checkForLocalMove(Car car, Track testTrack) {
2313        if (_train.isLocalSwitcher()) {
2314            // No local moves from spur to spur
2315            if (!Setup.isLocalSpurMovesEnabled() && testTrack.isSpur() && car.getTrack().isSpur()) {
2316                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoSpurToSpurMove"),
2317                        new Object[] { car.getTrackName(), testTrack.getName() }));
2318                return false;
2319            }
2320            // No local moves from yard to yard, except for cabooses and cars with FRED
2321            if (!Setup.isLocalYardMovesEnabled() &&
2322                    testTrack.isYard() &&
2323                    car.getTrack().isYard() &&
2324                    !car.isCaboose() &&
2325                    !car.hasFred()) {
2326                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoYardToYardMove"),
2327                        new Object[] { car.getTrackName(), testTrack.getName() }));
2328                return false;
2329            }
2330            // No local moves from interchange to interchange
2331            if (!Setup.isLocalInterchangeMovesEnabled() &&
2332                    testTrack.isInterchange() &&
2333                    car.getTrack().isInterchange()) {
2334                addLine(_buildReport, SEVEN,
2335                        MessageFormat.format(Bundle.getMessage("buildNoInterchangeToInterchangeMove"),
2336                                new Object[] { car.getTrackName(), testTrack.getName() }));
2337                return false;
2338            }
2339        }
2340        return true;
2341    }
2342
2343    protected Track tryStaging(Car car, RouteLocation rldSave) throws BuildFailedException {
2344        // local switcher working staging?
2345        if (_train.isLocalSwitcher() &&
2346                !car.isPassenger() &&
2347                !car.isCaboose() &&
2348                !car.hasFred() &&
2349                car.getTrack() == _terminateStageTrack) {
2350            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropCarSameTrack"),
2351                    new Object[] { car.toString(), car.getTrack().getName() }));
2352            return null;
2353        }
2354        // no need to check train and track direction into staging, already done
2355        String status = car.checkDestination(_terminateStageTrack.getLocation(), _terminateStageTrack);
2356        if (status.equals(Track.OKAY)) {
2357            return _terminateStageTrack;
2358            // only generate a new load if there aren't any other tracks available for this
2359            // car
2360        } else if (status.startsWith(Track.LOAD) &&
2361                car.getTrack() == _departStageTrack &&
2362                car.getLoadName().equals(carLoads.getDefaultEmptyName()) &&
2363                rldSave == null &&
2364                (_departStageTrack.isAddCustomLoadsAnyStagingTrackEnabled() ||
2365                        _departStageTrack.isAddCustomLoadsEnabled() ||
2366                        _departStageTrack.isAddCustomLoadsAnySpurEnabled())) {
2367            // try and generate a load for this car into staging
2368            if (generateLoadCarDepartingAndTerminatingIntoStaging(car, _terminateStageTrack)) {
2369                return _terminateStageTrack;
2370            }
2371        }
2372        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropCarBecause"), new Object[] {
2373                car.toString(), _terminateStageTrack.getName(), status, _terminateStageTrack.getTrackTypeName() }));
2374        return null;
2375    }
2376
2377    /**
2378     * Returns true if car can be picked up later in a train's route
2379     * 
2380     * @param car the car
2381     * @param rl  car's route location
2382     * @param rld car's route location destination
2383     * 
2384     * @return true if car can be picked up later in a train's route
2385     * @throws BuildFailedException if coding issue
2386     */
2387    protected boolean checkForLaterPickUp(Car car, RouteLocation rl, RouteLocation rld) throws BuildFailedException {
2388        if (rl != rld && rld.getName().equals(car.getLocationName())) {
2389            // don't delay adding a caboose, passenger car, or car with FRED
2390            if (car.isCaboose() || car.isPassenger() || car.hasFred()) {
2391                return false;
2392            }
2393            // no later pick up if car is departing staging
2394            if (car.getLocation().isStaging()) {
2395                return false;
2396            }
2397            if (!checkPickUpTrainDirection(car, rld)) {
2398                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoPickupLaterDirection"),
2399                        new Object[] { car.toString(), rld.getName(), rld.getId() }));
2400                return false;
2401            }
2402            if (!rld.isPickUpAllowed()) {
2403                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoPickupLater"),
2404                        new Object[] { car.toString(), rld.getName(), rld.getId() }));
2405                return false;
2406            }
2407            if (rld.getCarMoves() >= rld.getMaxCarMoves()) {
2408                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoPickupLaterMoves"),
2409                        new Object[] { car.toString(), rld.getName(), rld.getId() }));
2410                return false;
2411            }
2412            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildPickupLaterOkay"),
2413                    new Object[] { car.toString(), rld.getName(), rld.getId() }));
2414            return true;
2415        }
2416        return false;
2417    }
2418
2419    /**
2420     * Returns true is cars are allowed to travel from origin to terminal
2421     * 
2422     * @param car             The car
2423     * @param destinationName Destination name for this car
2424     * @return true if through cars are allowed. false if not.
2425     */
2426    protected boolean checkThroughCarsAllowed(Car car, String destinationName) {
2427        if (!_train.isAllowThroughCarsEnabled() &&
2428                !_train.isLocalSwitcher() &&
2429                !car.isCaboose() &&
2430                !car.hasFred() &&
2431                !car.isPassenger() &&
2432                splitString(car.getLocationName()).equals(splitString(_departLocation.getName())) &&
2433                splitString(destinationName).equals(splitString(_terminateLocation.getName())) &&
2434                !splitString(_departLocation.getName()).equals(splitString(_terminateLocation.getName()))) {
2435            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildThroughTrafficNotAllow"),
2436                    new Object[] { _departLocation.getName(), _terminateLocation.getName() }));
2437            return false; // through cars not allowed
2438        }
2439        return true; // through cars allowed
2440    }
2441
2442    private boolean checkLocalMovesAllowed(Car car, Track track) {
2443        if (!_train.isAllowLocalMovesEnabled() &&
2444                splitString(car.getLocationName()).equals(splitString(track.getLocation().getName()))) {
2445            addLine(_buildReport, SEVEN,
2446                    MessageFormat.format(Bundle.getMessage("buildNoLocalMoveToTrack"),
2447                            new Object[] { car.getLocationName(), car.getTrackName(), track.getLocation().getName(),
2448                                    track.getName(), _train.getName() }));
2449            return false;
2450        }
2451        return true;
2452    }
2453
2454    /**
2455     * Creates a car load for a car departing staging and eventually terminating
2456     * into staging.
2457     *
2458     * @param car        the car!
2459     * @param stageTrack the staging track the car will terminate to
2460     * @return true if a load was generated this this car.
2461     * @throws BuildFailedException if coding check fails
2462     */
2463    protected boolean generateLoadCarDepartingAndTerminatingIntoStaging(Car car, Track stageTrack)
2464            throws BuildFailedException {
2465        // code check
2466        if (stageTrack == null || !stageTrack.isStaging()) {
2467            throw new BuildFailedException("ERROR coding issue, staging track null or not staging");
2468        }
2469        if (!stageTrack.isTypeNameAccepted(car.getTypeName())) {
2470            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildStagingTrackType"),
2471                    new Object[] { stageTrack.getName(), car.getTypeName() }));
2472            return false;
2473        }
2474        if (!stageTrack.isRoadNameAccepted(car.getRoadName())) {
2475            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildStagingTrackRoad"),
2476                    new Object[] { stageTrack.getName(), car.getTypeName() }));
2477            return false;
2478        }
2479        // Departing and returning to same location in staging?
2480        if (!_train.isAllowReturnToStagingEnabled() &&
2481                !Setup.isStagingAllowReturnEnabled() &&
2482                !car.isCaboose() &&
2483                !car.hasFred() &&
2484                !car.isPassenger() &&
2485                splitString(car.getLocationName()).equals(splitString(stageTrack.getLocation().getName()))) {
2486            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildNoReturnStaging"),
2487                    new Object[] { car.toString(), stageTrack.getLocation().getName() }));
2488            return false;
2489        }
2490        // figure out which loads the car can use
2491        List<String> loads = carLoads.getNames(car.getTypeName());
2492        // remove the default names
2493        loads.remove(carLoads.getDefaultEmptyName());
2494        loads.remove(carLoads.getDefaultLoadName());
2495        if (loads.size() == 0) {
2496            log.debug("No custom loads for car type ({}) ignoring staging track ({})", car.getTypeName(),
2497                    stageTrack.getName());
2498            return false;
2499        }
2500        addLine(_buildReport, SEVEN, BLANK_LINE);
2501        addLine(_buildReport, SEVEN,
2502                MessageFormat.format(Bundle.getMessage("buildSearchTrackLoadStaging"),
2503                        new Object[] { car.toString(), car.getTypeName(), car.getLoadName(), car.getLocationName(),
2504                                car.getTrackName(), stageTrack.getLocation().getName(), stageTrack.getName() }));
2505        String oldLoad = car.getLoadName(); // save car's "E" load
2506        for (int i = loads.size() - 1; i >= 0; i--) {
2507            String load = loads.get(i);
2508            log.debug("Try custom load ({}) for car ({})", load, car.toString());
2509            if (!car.getTrack().isLoadNameAndCarTypeShipped(load, car.getTypeName()) ||
2510                    !stageTrack.isLoadNameAndCarTypeAccepted(load, car.getTypeName()) ||
2511                    !_train.isLoadNameAccepted(load, car.getTypeName())) {
2512                if (!car.getTrack().isLoadNameAndCarTypeShipped(load, car.getTypeName())) {
2513                    addLine(_buildReport, SEVEN,
2514                            MessageFormat.format(Bundle.getMessage("buildTrackNotNewLoad"),
2515                                    new Object[] { car.getTrackName(), load, stageTrack.getLocation().getName(),
2516                                            stageTrack.getName() }));
2517                }
2518                if (!stageTrack.isLoadNameAndCarTypeAccepted(load, car.getTypeName())) {
2519                    addLine(_buildReport, SEVEN,
2520                            MessageFormat.format(Bundle.getMessage("buildDestTrackNoLoad"), new Object[] {
2521                                    stageTrack.getLocation().getName(), stageTrack.getName(), car.toString(), load, }));
2522                }
2523                if (!_train.isLoadNameAccepted(load, car.getTypeName())) {
2524                    addLine(_buildReport, SEVEN,
2525                            MessageFormat.format(Bundle.getMessage("buildTrainNotNewLoad"),
2526                                    new Object[] { _train.getName(), load, stageTrack.getLocation().getName(),
2527                                            stageTrack.getName() }));
2528                }
2529                loads.remove(i);
2530                continue;
2531            }
2532            car.setLoadName(load);
2533            // does the car have a home division?
2534            if (car.getDivision() != null) {
2535                addLine(_buildReport, SEVEN,
2536                        MessageFormat.format(Bundle.getMessage("buildCarHasDivisionStaging"),
2537                                new Object[] { car.toString(), car.getTypeName(), car.getLoadType().toLowerCase(),
2538                                        car.getLoadName(), car.getDivisionName(), car.getLocationName(),
2539                                        car.getTrackName(), car.getTrack().getDivisionName() }));
2540                // load type empty must return to car's home division
2541                // or load type load from foreign division must return to car's home division
2542                if (car.getLoadType().equals(CarLoad.LOAD_TYPE_EMPTY) &&
2543                        car.getDivision() != stageTrack.getDivision() ||
2544                        car.getLoadType().equals(CarLoad.LOAD_TYPE_LOAD) &&
2545                                car.getTrack().getDivision() != car.getDivision() &&
2546                                car.getDivision() != stageTrack.getDivision()) {
2547                    addLine(_buildReport, SEVEN,
2548                            MessageFormat.format(Bundle.getMessage("buildNoDivisionTrack"),
2549                                    new Object[] { stageTrack.getTrackTypeName(), stageTrack.getLocation().getName(),
2550                                            stageTrack.getName(), stageTrack.getDivisionName(), car.toString(),
2551                                            car.getLoadType().toLowerCase(), car.getLoadName() }));
2552                    loads.remove(i);
2553                    continue;
2554                }
2555            }
2556            // are there trains that can carry the car type and load to the staging track?
2557            if (!router.isCarRouteable(car, _train, stageTrack, _buildReport)) {
2558                loads.remove(i); // no remove this load
2559                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildStagingTrackNotReachable"),
2560                        new Object[] { stageTrack.getLocation().getName(), stageTrack.getName(), load }));
2561            }
2562        }
2563        // Use random loads rather that the first one that works to create interesting
2564        // loads
2565        if (loads.size() > 0) {
2566            int rnd = (int) (Math.random() * loads.size());
2567            car.setLoadName(loads.get(rnd));
2568            // check to see if car is now accepted by staging
2569            String status = car.checkDestination(stageTrack.getLocation(), stageTrack); // will staging accept this car?
2570            if (status.equals(Track.OKAY) || (status.startsWith(Track.LENGTH) && stageTrack != _terminateStageTrack)) {
2571                car.setLoadGeneratedFromStaging(true);
2572                car.setFinalDestination(stageTrack.getLocation());
2573                // don't set track assignment unless the car is going to this train's staging
2574                if (stageTrack == _terminateStageTrack) {
2575                    car.setFinalDestinationTrack(stageTrack);
2576                } else {
2577                    car.setFinalDestinationTrack(null); // don't assign the track, that will be done later
2578                }
2579                car.updateKernel(); // is car part of kernel?
2580                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildAddingScheduleLoad"),
2581                        new Object[] { car.getLoadName(), car.toString() }));
2582                return true;
2583            }
2584            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropCarBecause"),
2585                    new Object[] { car.toString(), stageTrack.getName(), status, stageTrack.getTrackTypeName() }));
2586        }
2587        car.setLoadName(oldLoad); // restore load and report failure
2588        addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildUnableNewLoadStaging"), new Object[] {
2589                car.toString(), car.getTrackName(), stageTrack.getLocation().getName(), stageTrack.getName() }));
2590        return false;
2591    }
2592
2593    /**
2594     * Checks to see if cars that are already in the train can be redirected from
2595     * the alternate track to the spur that really wants the car. Fixes the issue of
2596     * having cars placed at the alternate when the spur's cars get pulled by this
2597     * train, but cars were sent to the alternate because the spur was full at the
2598     * time it was tested.
2599     *
2600     * @return true if one or more cars were redirected
2601     * @throws BuildFailedException if coding issue
2602     */
2603    protected boolean redirectCarsFromAlternateTrack() throws BuildFailedException {
2604        // code check, should be aggressive
2605        if (!Setup.isBuildAggressive()) {
2606            throw new BuildFailedException("ERROR coding issue, should be using aggressive mode");
2607        }
2608        boolean redirected = false;
2609        List<Car> cars = carManager.getByTrainList(_train);
2610        for (Car car : cars) {
2611            // does the car have a final destination and the destination is this one?
2612            if (car.getFinalDestination() == null ||
2613                    car.getFinalDestinationTrack() == null ||
2614                    !car.getFinalDestinationName().equals(car.getDestinationName())) {
2615                continue;
2616            }
2617            Track alternate = car.getFinalDestinationTrack().getAlternateTrack();
2618            if (alternate == null || car.getDestinationTrack() != alternate) {
2619                continue;
2620            }
2621            // is the car in a kernel?
2622            if (car.getKernel() != null && !car.isLead()) {
2623                continue;
2624            }
2625            log.debug("Car ({}) alternaten track ({}) has final destination track ({}) location ({})", car.toString(),
2626                    car.getDestinationTrackName(), car.getFinalDestinationTrackName(), car.getDestinationName()); // NOI18N
2627            if ((alternate.isYard() || alternate.isInterchange()) &&
2628                    car.checkDestination(car.getFinalDestination(), car.getFinalDestinationTrack())
2629                            .equals(Track.OKAY) &&
2630                    checkDropTrainDirection(car, car.getRouteDestination(), car.getFinalDestinationTrack()) &&
2631                    checkTrainCanDrop(car, car.getFinalDestinationTrack())) {
2632                log.debug("Car ({}) alternate track ({}) can be redirected to final destination track ({})",
2633                        car.toString(), car.getDestinationTrackName(), car.getFinalDestinationTrackName());
2634                if (car.getKernel() != null) {
2635                    for (Car k : car.getKernel().getCars()) {
2636                        if (k.isLead()) {
2637                            continue;
2638                        }
2639                        addLine(_buildReport, FIVE,
2640                                MessageFormat.format(Bundle.getMessage("buildRedirectFromAlternate"),
2641                                        new Object[]{car.getFinalDestinationName(),
2642                                                car.getFinalDestinationTrackName(), k.toString(),
2643                                                car.getDestinationTrackName()}));
2644                        // force car to track
2645                        k.setDestination(car.getFinalDestination(), car.getFinalDestinationTrack(), true);
2646                    }
2647                }
2648                addLine(_buildReport, FIVE,
2649                        MessageFormat.format(Bundle.getMessage("buildRedirectFromAlternate"),
2650                                new Object[]{car.getFinalDestinationName(), car.getFinalDestinationTrackName(),
2651                                        car.toString(), car.getDestinationTrackName()}));
2652                car.setDestination(car.getFinalDestination(), car.getFinalDestinationTrack(), true);
2653                redirected = true;
2654            }
2655        }
2656        return redirected;
2657    }
2658
2659    /**
2660     * report any cars left at route location
2661     * 
2662     * @param rl route location
2663     */
2664    protected void showCarsNotMoved(RouteLocation rl) {
2665        if (_carIndex < 0) {
2666            _carIndex = 0;
2667        }
2668        // cars up this point have build report messages, only show the cars that aren't
2669        // in the build report
2670        int numberCars = 0;
2671        for (int i = _carIndex; i < _carList.size(); i++) {
2672            if (numberCars == DISPLAY_CAR_LIMIT_100) {
2673                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildOnlyFirstXXXCars"),
2674                        new Object[] { numberCars, rl.getName() }));
2675                break;
2676            }
2677            Car car = _carList.get(i);
2678            // find a car at this location that hasn't been given a destination
2679            if (!car.getLocationName().equals(rl.getName()) || car.getRouteDestination() != null) {
2680                continue;
2681            }
2682            if (numberCars == 0) {
2683                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildMovesCompleted"),
2684                        new Object[] { rl.getMaxCarMoves(), rl.getName() }));
2685            }
2686            addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCarIgnored"), new Object[] {
2687                    car.toString(), car.getTypeName(), car.getLoadName(), car.getLocationName(), car.getTrackName() }));
2688            numberCars++;
2689        }
2690        addLine(_buildReport, SEVEN, BLANK_LINE);
2691    }
2692
2693
2694
2695    /**
2696     * Remove rolling stock from train
2697     * 
2698     * @param rs the rolling stock to be removed
2699     */
2700    protected void removeRollingStockFromTrain(RollingStock rs) {
2701        // adjust train length and weight for each location that the rolling stock is in
2702        // the train
2703        boolean inTrain = false;
2704        for (RouteLocation routeLocation : _routeList) {
2705            if (rs.getRouteLocation() == routeLocation) {
2706                inTrain = true;
2707            }
2708            if (rs.getRouteDestination() == routeLocation) {
2709                break;
2710            }
2711            if (inTrain) {
2712                routeLocation.setTrainLength(routeLocation.getTrainLength() - rs.getTotalLength()); // includes couplers
2713                routeLocation.setTrainWeight(routeLocation.getTrainWeight() - rs.getAdjustedWeightTons());
2714            }
2715        }
2716        rs.reset(); // remove this rolling stock from the train
2717    }
2718
2719
2720    /**
2721     * Lists cars that couldn't be routed.
2722     */
2723    protected void showCarsNotRoutable() {
2724        // any cars not able to route?
2725        if (_notRoutable.size() > 0) {
2726            addLine(_buildReport, ONE, BLANK_LINE);
2727            addLine(_buildReport, ONE, Bundle.getMessage("buildCarsNotRoutable"));
2728            for (Car car : _notRoutable) {
2729                _warnings++;
2730                addLine(_buildReport, ONE,
2731                        MessageFormat.format(Bundle.getMessage("buildCarNotRoutable"),
2732                                new Object[] { car.toString(), car.getLocationName(), car.getTrackName(),
2733                                        car.getFinalDestinationName(), car.getFinalDestinationTrackName() }));
2734            }
2735            addLine(_buildReport, ONE, BLANK_LINE);
2736        }
2737    }
2738
2739    /**
2740     * build has failed due to cars in staging not having destinations this routine
2741     * removes those cars from the staging track by user request.
2742     * 
2743     */
2744    protected void removeCarsFromStaging() {
2745        // Code check, only called if train was departing staging
2746        if (_departStageTrack == null) {
2747            log.error("Error, called when cars in staging not assigned to train");
2748            return;
2749        }
2750        for (Car car : _carList) {
2751            // remove cars from departure staging track that haven't been assigned to this
2752            // train
2753            if (car.getTrack() == _departStageTrack && car.getTrain() == null) {
2754                // remove track from kernel
2755                if (car.getKernel() != null) {
2756                    for (Car c : car.getKernel().getCars())
2757                        c.setLocation(car.getLocation(), null);
2758                } else {
2759                    car.setLocation(car.getLocation(), null);
2760                }
2761            }
2762        }
2763    }
2764    
2765    /*
2766     *  Engine methods start here
2767     */
2768    
2769
2770    
2771    /**
2772     * Adds engines to the train if needed based on HPT. Note that the engine
2773     * additional weight isn't considered in this method so HP requirements can be
2774     * lower compared to the original calculation which did include the weight of
2775     * the engines.
2776     * 
2777     * @param hpAvailable   the engine hp already assigned to the train for this leg
2778     * @param extraHpNeeded the additional hp needed
2779     * @param rlNeedHp      where in the route the additional hp is needed
2780     * @param rl            the start of the leg
2781     * @param rld           the end of the leg
2782     * @throws BuildFailedException if unable to add engines to train
2783     */
2784    protected void addEnginesBasedHPT(int hpAvailable, int extraHpNeeded, RouteLocation rlNeedHp, RouteLocation rl,
2785            RouteLocation rld) throws BuildFailedException {
2786        if (rlNeedHp == null) {
2787            return;
2788        }
2789        int numberLocos = 0;
2790        // determine how many locos have already been assigned to the train
2791        List<Engine> engines = engineManager.getList(_train);
2792        for (Engine rs : engines) {
2793            if (rs.getRouteLocation() == rl) {
2794                numberLocos++;
2795            }
2796        }
2797
2798        addLine(_buildReport, ONE, BLANK_LINE);
2799        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildTrainReqExtraHp"),
2800                new Object[] { extraHpNeeded, rlNeedHp.getName(), rld.getName(), numberLocos }));
2801
2802        // determine engine model and road
2803        String model = _train.getEngineModel();
2804        String road = _train.getEngineRoad();
2805        if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
2806                rl == _train.getSecondLegStartRouteLocation()) {
2807            model = _train.getSecondLegEngineModel();
2808            road = _train.getSecondLegEngineRoad();
2809        } else if ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
2810                rl == _train.getThirdLegStartRouteLocation()) {
2811            model = _train.getThirdLegEngineModel();
2812            road = _train.getThirdLegEngineRoad();
2813        }
2814
2815        while (numberLocos < Setup.getMaxNumberEngines()) {
2816            // if no engines assigned, can't use B unit as first engine
2817            if (!getEngines("1", model, road, rl, rld, numberLocos > 0)) {
2818                throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorEngines"),
2819                        new Object[] { Bundle.getMessage("additional"), rl.getName(), rld.getName() }));
2820            }
2821            numberLocos++;
2822            int currentHp = _train.getTrainHorsePower(rlNeedHp);
2823            if (currentHp > hpAvailable + extraHpNeeded) {
2824                break; // done
2825            }
2826            if (numberLocos < Setup.getMaxNumberEngines()) {
2827                addLine(_buildReport, FIVE, BLANK_LINE);
2828                addLine(_buildReport, THREE,
2829                        MessageFormat.format(Bundle.getMessage("buildContinueAddLocos"),
2830                                new Object[] { (hpAvailable + extraHpNeeded - currentHp), rlNeedHp.getName(),
2831                                        rld.getName(), numberLocos, currentHp }));
2832            } else {
2833                addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildMaxNumberLocoAssigned"),
2834                        new Object[] { Setup.getMaxNumberEngines() }));
2835            }
2836        }
2837    }
2838
2839    /**
2840     * Adds an engine to the train.
2841     * 
2842     * @param engine the engine being added to the train
2843     * @param rl     where in the train's route to pick up the engine
2844     * @param rld    where in the train's route to set out the engine
2845     * @param track  the destination track for this engine
2846     */
2847    private void addEngineToTrain(Engine engine, RouteLocation rl, RouteLocation rld, Track track) {
2848        _lastEngine = engine; // needed in case there's a engine change in the train's route
2849        if (_train.getLeadEngine() == null) {
2850            _train.setLeadEngine(engine); // load lead engine
2851        }
2852        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildEngineAssigned"),
2853                new Object[] { engine.toString(), rl.getName(), rld.getName(), track.getName() }));
2854        engine.setDestination(track.getLocation(), track);
2855        int length = engine.getTotalLength();
2856        int weightTons = engine.getAdjustedWeightTons();
2857        // engine in consist?
2858        if (engine.getConsist() != null) {
2859            length = engine.getConsist().getTotalLength();
2860            weightTons = engine.getConsist().getAdjustedWeightTons();
2861            for (Engine cEngine : engine.getConsist().getEngines()) {
2862                if (cEngine != engine) {
2863                    addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildEngineAssigned"),
2864                            new Object[] { cEngine.toString(), rl.getName(), rld.getName(), track.getName() }));
2865                    cEngine.setTrain(_train);
2866                    cEngine.setRouteLocation(rl);
2867                    cEngine.setRouteDestination(rld);
2868                    cEngine.setDestination(track.getLocation(), track, true); // force destination
2869                }
2870            }
2871        }
2872        // now adjust train length and weight for each location that engines are in the
2873        // train
2874        finishAddRsToTrain(engine, rl, rld, length, weightTons);
2875    }
2876    
2877    private boolean buildConsistFromSingleLocos(int reqNumberEngines, List<Engine> singleLocos, RouteLocation rl,
2878            RouteLocation rld) {
2879        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildOptionBuildConsist"),
2880                new Object[] { reqNumberEngines, rl.getName() }));
2881        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildOptionSingleLocos"),
2882                new Object[] { singleLocos.size(), rl.getName() }));
2883        if (singleLocos.size() >= reqNumberEngines) {
2884            int locos = 0;
2885            // first find an "A" unit
2886            for (Engine engine : singleLocos) {
2887                if (engine.isBunit()) {
2888                    continue;
2889                }
2890                if (setEngineDestination(engine, rl, rld)) {
2891                    _engineList.remove(engine);
2892                    singleLocos.remove(engine);
2893                    locos++;
2894                    break; // found "A" unit
2895                }
2896            }
2897            // did we find an "A" unit?
2898            if (locos > 0) {
2899                // now add the rest "A" or "B" units
2900                for (Engine engine : singleLocos) {
2901                    if (setEngineDestination(engine, rl, rld)) {
2902                        _engineList.remove(engine);
2903                        locos++;
2904                    }
2905                    if (locos == reqNumberEngines) {
2906                        return true; // done!
2907                    }
2908                }
2909            } else {
2910                // list the "B" units found
2911                for (Engine engine : singleLocos) {
2912                    if (engine.isBunit()) {
2913                        addLine(_buildReport, FIVE,
2914                                MessageFormat.format(Bundle.getMessage("BuildEngineBunit"), new Object[] {
2915                                        engine.toString(), engine.getLocationName(), engine.getTrackName() }));
2916                    }
2917                }
2918            }
2919        }
2920        return false;
2921    }
2922    
2923    /**
2924     * Used to determine the number of engines requested by the user.
2925     * 
2926     * @param requestEngines Can be a number, AUTO or AUTO HPT.
2927     * @return the number of engines requested by user.
2928     */
2929    protected int getNumberEngines(String requestEngines) {
2930        int numberEngines = 0;
2931        if (requestEngines.equals(Train.AUTO)) {
2932            numberEngines = getAutoEngines();
2933        } else if (requestEngines.equals(Train.AUTO_HPT)) {
2934            numberEngines = 1; // get one loco for now, check HP requirements after train is built
2935        } else {
2936            numberEngines = Integer.parseInt(requestEngines);
2937        }
2938        return numberEngines;
2939    }
2940
2941    /**
2942     * Sets the destination track for an engine and assigns it to the train.
2943     * 
2944     * @param engine The engine to be added to train
2945     * @param rl     Departure route location
2946     * @param rld    Destination route location
2947     * @return true if destination track found and set
2948     */
2949    protected boolean setEngineDestination(Engine engine, RouteLocation rl, RouteLocation rld) {
2950        // engine to staging?
2951        if (rld == _train.getTrainTerminatesRouteLocation() && _terminateStageTrack != null) {
2952            String status = engine.checkDestination(_terminateStageTrack.getLocation(), _terminateStageTrack);
2953            if (status.equals(Track.OKAY)) {
2954                addEngineToTrain(engine, rl, rld, _terminateStageTrack);
2955                return true; // done
2956            } else {
2957                addLine(_buildReport, SEVEN,
2958                        MessageFormat.format(Bundle.getMessage("buildCanNotDropEngineToTrack"),
2959                                new Object[] { engine.toString(), _terminateStageTrack.getName(), status,
2960                                        _terminateStageTrack.getTrackTypeName() }));
2961            }
2962        } else {
2963            // find a destination track for this engine
2964            Location destination = rld.getLocation();
2965            List<Track> destTracks = destination.getTracksByMoves(null);
2966            if (destTracks.size() == 0) {
2967                addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildNoTracksAtDestination"),
2968                        new Object[] { rld.getName() }));
2969            }
2970            for (Track track : destTracks) {
2971                if (!checkDropTrainDirection(engine, rld, track)) {
2972                    continue;
2973                }
2974                String status = engine.checkDestination(destination, track);
2975                if (status.equals(Track.OKAY)) {
2976                    addEngineToTrain(engine, rl, rld, track);
2977                    return true;
2978                } else {
2979                    addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildCanNotDropEngineToTrack"),
2980                            new Object[] { engine.toString(), track.getName(), status, track.getTrackTypeName() }));
2981                }
2982            }
2983            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildCanNotDropEngToDest"),
2984                    new Object[] { engine.toString(), rld.getName() }));
2985        }
2986        return false; // not able to set loco's destination
2987    }
2988
2989    /**
2990     * Returns the number of engines needed for this train, minimum 1, maximum user
2991     * specified in setup. Based on maximum allowable train length and grade between
2992     * locations, and the maximum cars that the train can have at the maximum train
2993     * length. One engine per sixteen 40' cars for 1% grade. TODO Currently ignores
2994     * the cars weight and engine horsepower
2995     *
2996     * @return The number of engines needed
2997     */
2998    private int getAutoEngines() {
2999        double numberEngines = 1;
3000        int moves = 0;
3001        int carLength = 40 + Car.COUPLERS; // typical 40' car
3002
3003        // adjust if length in meters
3004        if (!Setup.getLengthUnit().equals(Setup.FEET)) {
3005            carLength = 12 + Car.COUPLERS; // typical car in meters
3006        }
3007
3008        for (RouteLocation rl : _routeList) {
3009            if (rl.isPickUpAllowed()) {
3010                moves += rl.getMaxCarMoves(); // assume all moves are pick ups
3011                double carDivisor = 16; // number of 40' cars per engine 1% grade
3012                // change engine requirements based on grade
3013                if (rl.getGrade() > 1) {
3014                    carDivisor = carDivisor / rl.getGrade();
3015                }
3016                log.debug("Maximum train length {} for location ({})", rl.getMaxTrainLength(), rl.getName());
3017                if (rl.getMaxTrainLength() / (carDivisor * carLength) > numberEngines) {
3018                    numberEngines = rl.getMaxTrainLength() / (carDivisor * carLength);
3019                    // round up to next whole integer
3020                    numberEngines = Math.ceil(numberEngines);
3021                    // determine if there's enough car pick ups at this point to reach the max train
3022                    // length
3023                    if (numberEngines > moves / carDivisor) {
3024                        numberEngines = Math.ceil(moves / carDivisor); // no reduce based on moves
3025                    }
3026                }
3027            }
3028        }
3029        int nE = (int) numberEngines;
3030        addLine(_buildReport, ONE,
3031                MessageFormat.format(Bundle.getMessage("buildAutoBuildMsg"), new Object[] { Integer.toString(nE) }));
3032        if (nE > Setup.getMaxNumberEngines()) {
3033            addLine(_buildReport, THREE, MessageFormat.format(Bundle.getMessage("buildMaximumNumberEngines"),
3034                    new Object[] { Setup.getMaxNumberEngines() }));
3035            nE = Setup.getMaxNumberEngines();
3036        }
3037        return nE;
3038    }
3039    
3040    protected boolean getConsist(String reqNumEngines, String model, String road, RouteLocation rl, RouteLocation rld)
3041            throws BuildFailedException {
3042        if (reqNumEngines.equals(Train.AUTO_HPT)) {
3043            for (int i = 2; i < Setup.getMaxNumberEngines(); i++) {
3044                if (getEngines(Integer.toString(i), model, road, rl, rld)) {
3045                    return true;
3046                }
3047            }
3048        }
3049        return false;
3050    }
3051    
3052
3053    
3054    protected void showEnginesByLocation() {
3055        // show how many engines were found
3056        addLine(_buildReport, FIVE, BLANK_LINE);
3057        addLine(_buildReport, ONE, MessageFormat.format(Bundle.getMessage("buildFoundLocos"),
3058                new Object[] { Integer.toString(_engineList.size()), _train.getName() }));
3059
3060        List<String> locationNames = new ArrayList<>(); // only show engines once using the train's route
3061        for (RouteLocation rl : _train.getRoute().getLocationsBySequenceList()) {
3062            if (locationNames.contains(rl.getName())) {
3063                continue;
3064            }
3065            locationNames.add(rl.getName());
3066            if (rl.getLocation().isStaging()) {
3067                addLine(_buildReport, SEVEN,
3068                        MessageFormat.format(Bundle.getMessage("buildLocosInStaging"), new Object[] { rl.getName() }));
3069            } else {
3070                addLine(_buildReport, SEVEN,
3071                        MessageFormat.format(Bundle.getMessage("buildLocosAtLocation"), new Object[] { rl.getName() }));
3072            }
3073            for (Engine engine : _engineList) {
3074                if (engine.getLocationName().equals(rl.getName())) {
3075                    addLine(_buildReport, SEVEN,
3076                            MessageFormat.format(Bundle.getMessage("buildLocoAtLocWithMoves"),
3077                                    new Object[]{engine.toString(), engine.getTypeName(), engine.getModel(),
3078                                            engine.getLocationName(), engine.getTrackName(), engine.getMoves()}));
3079                }
3080            }
3081            addLine(_buildReport, FIVE, BLANK_LINE);
3082        }
3083    }
3084
3085    protected boolean getEngines(String requestedEngines, String model, String road, RouteLocation rl,
3086            RouteLocation rld) throws BuildFailedException {
3087        return getEngines(requestedEngines, model, road, rl, rld, !USE_BUNIT);
3088    }
3089
3090    /**
3091     * Get the engines for this train at a route location. If departing from staging
3092     * engines must come from that track. Finds the required number of engines in a
3093     * consist, or if the option to build from single locos, builds a consist for
3094     * the user. When true, engines successfully added to train for the leg
3095     * requested.
3096     * 
3097     * @param requestedEngines Requested number of Engines, can be number, AUTO or
3098     *                         AUTO HPT
3099     * @param model            Optional model name for the engines
3100     * @param road             Optional road name for the engines
3101     * @param rl               Departure route location for the engines
3102     * @param rld              Destination route location for the engines
3103     * @param useBunit         true if B unit engine is allowed
3104     * @return true if correct number of engines found.
3105     * @throws BuildFailedException if coding issue
3106     */
3107    protected boolean getEngines(String requestedEngines, String model, String road, RouteLocation rl,
3108            RouteLocation rld, boolean useBunit) throws BuildFailedException {
3109        // load departure track if staging
3110        Track departStageTrack = null;
3111        if (rl == _train.getTrainDepartsRouteLocation()) {
3112            departStageTrack = _departStageTrack; // get departure track from staging, could be null
3113        }
3114
3115        int reqNumberEngines = getNumberEngines(requestedEngines);
3116
3117        // if not departing staging track and engines aren't required done!
3118        if (departStageTrack == null && reqNumberEngines == 0) {
3119            return true;
3120        }
3121        // if departing staging and no engines required and none available, we're done
3122        if (departStageTrack != null && reqNumberEngines == 0 && departStageTrack.getNumberEngines() == 0) {
3123            return true;
3124        }
3125
3126        // code check, staging track selection checks number of engines needed
3127        if (departStageTrack != null &&
3128                reqNumberEngines != 0 &&
3129                departStageTrack.getNumberEngines() != reqNumberEngines) {
3130            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildStagingNotEngines"),
3131                    new Object[] { departStageTrack.getName(), departStageTrack.getNumberEngines(), reqNumberEngines }));
3132        }
3133
3134        // code check
3135        if (rl == null || rld == null) {
3136            throw new BuildFailedException(
3137                    MessageFormat.format(Bundle.getMessage("buildErrorEngLocUnknown"), new Object[] {}));
3138        }
3139
3140        addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildBegineSearchEngines"),
3141                new Object[] { reqNumberEngines, model, road, rl.getName(), rld.getName() }));
3142
3143        int assignedLocos = 0; // the number of locos assigned to this train
3144        List<Engine> singleLocos = new ArrayList<>();
3145        for (int indexEng = 0; indexEng < _engineList.size(); indexEng++) {
3146            Engine engine = _engineList.get(indexEng);
3147            log.debug("Engine ({}) at location ({}, {})", engine.toString(), engine.getLocationName(),
3148                    engine.getTrackName());
3149
3150            // use engines that are departing from the selected staging track (departTrack
3151            // != null if staging)
3152            if (departStageTrack != null && !departStageTrack.equals(engine.getTrack())) {
3153                continue;
3154            }
3155            // use engines that are departing from the correct location
3156            if (!engine.getLocationName().equals(rl.getName())) {
3157                log.debug("Skipping engine ({}) at location ({})", engine.toString(), engine.getLocationName());
3158                continue;
3159            }
3160            // skip engines models that train does not service
3161            if (!model.equals(Train.NONE) && !engine.getModel().equals(model)) {
3162                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeEngineModel"),
3163                        new Object[] { engine.toString(), engine.getModel(), engine.getLocationName() }));
3164                continue;
3165            }
3166            // Does the train have a very specific engine road name requirement?
3167            if (!road.equals(Train.NONE) && !engine.getRoadName().equals(road)) {
3168                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeEngineRoad"),
3169                        new Object[]{engine.toString(), engine.getLocationName(), engine.getTrackName(),
3170                                engine.getRoadName()}));
3171                continue;
3172            }
3173            // skip engines on tracks that don't service the train's departure direction
3174            if (!checkPickUpTrainDirection(engine, rl)) {
3175                continue;
3176            }
3177            // skip engines that have been assigned destinations that don't match the
3178            // requested destination
3179            if (engine.getDestination() != null && !engine.getDestinationName().equals(rld.getName())) {
3180                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeEngineDestination"),
3181                        new Object[] { engine.toString(), engine.getDestinationName() }));
3182                continue;
3183            }
3184            // don't use non lead locos in a consist
3185            if (engine.getConsist() != null) {
3186                if (engine.isLead()) {
3187                    addLine(_buildReport, SEVEN,
3188                            MessageFormat.format(Bundle.getMessage("buildEngineLeadConsist"),
3189                                    new Object[]{engine.toString(), engine.getConsist().getName(),
3190                                            engine.getConsist().getEngines().size()}));      
3191                } else {
3192                    continue;
3193                }
3194            }
3195            // departing staging, then all locos must go!
3196            if (departStageTrack != null) {
3197                if (!setEngineDestination(engine, rl, rld)) {
3198                    return false;
3199                }
3200                _engineList.remove(indexEng--);
3201                if (engine.getConsist() != null) {
3202                    assignedLocos = assignedLocos + engine.getConsist().getSize();
3203                } else {
3204                    assignedLocos++;
3205                }
3206                continue;
3207            }
3208            // can't use B units if requesting one loco
3209            if (!useBunit && reqNumberEngines == 1 && engine.isBunit()) {
3210                addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeEngineBunit"),
3211                        new Object[] { engine.toString(), engine.getModel() }));
3212                continue;
3213            }
3214            // is this engine part of a consist?
3215            if (engine.getConsist() == null) {
3216                // single engine, but does the train require a consist?
3217                if (reqNumberEngines > 1) {
3218                    addLine(_buildReport, SEVEN, MessageFormat.format(Bundle.getMessage("buildExcludeEngineSingle"),
3219                            new Object[] { engine.toString(), reqNumberEngines }));
3220                    singleLocos.add(engine);
3221                    continue;
3222                }
3223            // engine is part of a consist
3224            } else if (engine.getConsist().getSize() == reqNumberEngines) {
3225                log.debug("Consist ({}) has the required number of engines", engine.getConsist().getName()); // NOI18N
3226            } else if (reqNumberEngines != 0) {
3227                addLine(_buildReport, SEVEN,
3228                        MessageFormat.format(Bundle.getMessage("buildExcludeEngConsistNumber"), new Object[]{
3229                                engine.toString(), engine.getConsist().getName(), engine.getConsist().getSize()}));
3230                continue;
3231            }
3232            // found a loco or consist!
3233            assignedLocos++;
3234
3235            // now find terminal track for engine(s)
3236            addLine(_buildReport, FIVE,
3237                    MessageFormat.format(Bundle.getMessage("buildEngineRoadModelType"),
3238                            new Object[] { engine.toString(), engine.getRoadName(), engine.getModel(),
3239                                    engine.getTypeName(), engine.getLocationName(), engine.getTrackName(),
3240                                    rld.getName() }));
3241            if (setEngineDestination(engine, rl, rld)) {
3242                _engineList.remove(indexEng--);
3243                return true; // normal exit when not staging
3244            }
3245        }
3246        // build a consist out of non-consisted locos
3247        if (assignedLocos == 0 && reqNumberEngines > 1 && _train.isBuildConsistEnabled()) {
3248            if (buildConsistFromSingleLocos(reqNumberEngines, singleLocos, rl,rld)) {
3249                return true; // normal exit when building with single locos
3250            }
3251        }
3252        if (assignedLocos == 0) {
3253            String locationName = rl.getName();
3254            if (departStageTrack != null) {
3255                locationName = locationName + ", " + departStageTrack.getName();
3256            }
3257            addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildNoLocosFoundAtLocation"),
3258                    new Object[] { locationName }));
3259        } else if (departStageTrack != null && (reqNumberEngines == 0 || reqNumberEngines == assignedLocos)) {
3260            return true; // normal exit assigning from staging
3261        }
3262        // not able to assign engines to train
3263        return false;
3264    }
3265    
3266    /**
3267     * Removes engine from train and attempts to replace it with engine or consist
3268     * that meets the HP requirements of the train.
3269     * 
3270     * @param hpNeeded   How much hp is needed
3271     * @param leadEngine The lead engine for this leg
3272     * @param model      The engine's model
3273     * @param road       The engine's road
3274     * @throws BuildFailedException if new engine not found
3275     */
3276    protected void getNewEngine(int hpNeeded, Engine leadEngine, String model, String road)
3277            throws BuildFailedException {
3278        // save lead engine's rl, and rld
3279        RouteLocation rl = leadEngine.getRouteLocation();
3280        RouteLocation rld = leadEngine.getRouteDestination();
3281        removeEngineFromTrain(leadEngine);
3282        _engineList.add(0, leadEngine); // put engine back into the pool
3283        if (hpNeeded < 50) {
3284            hpNeeded = 50; // the minimum HP
3285        }
3286        int hpMax = hpNeeded;
3287        // largest single engine HP known today is less than 15,000.
3288        // high end modern diesel locos approximately 5000 HP.
3289        // 100 car train at 100 tons per car and 2 HPT requires 20,000 HP.
3290        // will assign consisted engines to train.
3291        boolean foundLoco = false;
3292        hpLoop: while (hpMax < 20000) {
3293            hpMax += hpNeeded / 2; // start off looking for an engine with no more than 50% extra HP
3294            log.debug("Max hp {}", hpMax);
3295            for (Engine engine : _engineList) {
3296                // don't use non lead locos in a consist
3297                if (engine.getConsist() != null && !engine.isLead()) {
3298                    continue;
3299                }
3300                if (engine.getLocation() != rl.getLocation()) {
3301                    continue;
3302                }
3303                if (!model.equals(Train.NONE) && !engine.getModel().equals(model)) {
3304                    continue;
3305                }
3306                if (!road.equals(Train.NONE) && !engine.getRoadName().equals(road) ||
3307                        road.equals(Train.NONE) && !_train.isLocoRoadNameAccepted(engine.getRoadName())) {
3308                    continue;
3309                }
3310                int engineHp = engine.getHpInteger();
3311                if (engine.getConsist() != null) {
3312                    for (Engine e : engine.getConsist().getEngines()) {
3313                        if (e != engine) {
3314                            engineHp = engineHp + e.getHpInteger();
3315                        }
3316                    }
3317                }
3318                if (engineHp > hpNeeded && engineHp <= hpMax) {
3319                    addLine(_buildReport, FIVE, MessageFormat.format(Bundle.getMessage("buildLocoHasRequiredHp"),
3320                            new Object[] { engine.toString(), engineHp, hpNeeded }));
3321                    if (setEngineDestination(engine, rl, rld)) {
3322                        foundLoco = true;
3323                        break hpLoop;
3324                    }
3325                }
3326            }
3327        }
3328        if (!foundLoco && !_train.isBuildConsistEnabled()) {
3329            throw new BuildFailedException(MessageFormat.format(Bundle.getMessage("buildErrorEngHp"),
3330                    new Object[] { rl.getLocation().getName() }));
3331        }
3332    }
3333
3334    protected void removeEngineFromTrain(Engine engine) {
3335        // replace lead engine?
3336        if (_train.getLeadEngine() == engine) {
3337            _train.setLeadEngine(null);
3338        }
3339        if (engine.getConsist() != null) {
3340            for (Engine e : engine.getConsist().getEngines()) {
3341                removeRollingStockFromTrain(e);
3342            }
3343        } else {
3344            removeRollingStockFromTrain(engine);
3345        }
3346    }
3347
3348
3349    private final static Logger log = LoggerFactory.getLogger(TrainBuilderBase.class);
3350
3351}