001package jmri.jmrit.operations.trains.trainbuilder;
002
003import java.io.IOException;
004import java.util.Date;
005import java.util.List;
006
007import jmri.InstanceManager;
008import jmri.jmrit.operations.locations.Location;
009import jmri.jmrit.operations.locations.Track;
010import jmri.jmrit.operations.rollingstock.cars.Car;
011import jmri.jmrit.operations.routes.RouteLocation;
012import jmri.jmrit.operations.setup.Setup;
013import jmri.jmrit.operations.trains.*;
014import jmri.jmrit.operations.trains.csv.TrainCsvManifest;
015import jmri.jmrit.operations.trains.manualtrainbuilder.*;
016import jmri.util.swing.JmriJOptionPane;
017
018/**
019 * Builds a train and then creates the train's manifest.
020 *
021 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013,
022 *         2014, 2015, 2021, 2026
023 */
024public class TrainBuilder extends TrainBuilderCars {
025
026    /**
027     * Build rules:
028     * <ol>
029     * <li>Need at least one location in route to build train
030     * <li>Select only locos and cars that the train can service
031     * <li>If required, add caboose or car with FRED to train
032     * <li>When departing staging find a track matching train requirements
033     * <li>All cars and locos on one track must leave staging
034     * <li>Optionally block cars from staging
035     * <li>Route cars with home divisions
036     * <li>Route cars with custom loads or final destinations.
037     * <li>Service locations based on train direction, location car types, roads
038     * and loads.
039     * <li>Ignore track direction when train is a local (serves one location)
040     * </ol>
041     * <p>
042     * History:
043     * <p>
044     * First version of train builder found cars along a train's route and
045     * assigned destinations (tracks) willing to accept the car. This is called
046     * the random method as cars just bounce around the layout without purpose.
047     * Afterwards custom loads and routing was added to the program. Cars with
048     * custom loads or final destinations move with purpose as those cars are
049     * routed. The last major feature added was car divisions. Cars assigned a
050     * division are always routed.
051     * <p>
052     * The program was written around the concept of a build report. The report
053     * provides a description of the train build process and the steps taken to
054     * place rolling stock in a train. The goal was to help users understand why
055     * rolling stock was either assigned to the train or not, and which choices
056     * the program had available when determining an engine's or car's
057     * destination.
058     *
059     * @param train the train that is to be built
060     * @return True if successful.
061     */
062    public boolean build(Train train) {
063        setTrain(train);
064        try {
065            build();
066            return true;
067        } catch (BuildFailedException e) {
068            buildFailed(e);
069            return false;
070        }
071    }
072
073    private void build() throws BuildFailedException {
074        setStartTime(new Date());
075        log.debug("Building train ({})", getTrain().getName());
076
077        getTrain().setStatusCode(Train.CODE_BUILDING);
078        getTrain().setBuilt(false);
079        getTrain().setLeadEngine(null);
080
081        createBuildReportFile(); // backup build report and create new
082        showBuildReportInfo(); // add the build report header information
083        setUpRoute(); // load route, departure and terminate locations
084        showTrainBuildOptions(); // show the build options
085        showSpecificTrainBuildOptions(); // show the train build options
086        showAndInitializeTrainRoute(); // show the train's route and initialize
087        showIfLocalSwitcher(); // show if this train a switcher
088        showTrainRequirements(); // show how many engines, caboose, FRED changes
089        showTrainServices(); // engine roads, owners, built dates, and types
090        getAndRemoveEnginesFromList(); // get a list of available engines
091        showEnginesByLocation(); // list available engines by location
092        determineIfTrainTerminatesIntoStaging(); // find staging terminus track
093        determineIfTrainDepartsStagingAndAddEngines(); // add engines if staging
094        addEnginesToTrain(); // 1st, 2nd and 3rd engine swaps in a train's route
095        showTrainCarRoads(); // show car roads that this train will service
096        showTrainCabooseRoads(); // show caboose roads that this train will service
097        showTrainCarTypes(); // show car types that this train will service
098        showTrainLoadNames(); // show load names that this train will service
099        createCarList(); // remove unwanted cars
100        adjustCarsInStaging(); // adjust for cars on one staging track
101        showCarsByLocation(); // list available cars by location
102        sortCarsOnFifoLifoTracks(); // sort cars on FIFO or LIFO tracks
103        saveCarFinalDestinations(); // save car's final dest and schedule id
104        addCabooseOrFredToTrain(); // caboose and FRED changes
105        manualBuild(); // adds cars to train based on user's requests
106        removeCaboosesAndCarsWithFred(); // done with cabooses and FRED
107        blockCarsFromStaging(); // block cars from staging
108        showTracksNotQuickService(); // list tracks that aren't using quick service
109        
110        addCarsToTrain(); // finds and adds cars to the train (main routine)
111
112        checkStuckCarsInStaging(); // determine if cars are stuck in staging
113        setTrainBuildStatus(); // show how well the build went
114        checkEngineHP(); // determine if train has appropriate engine HP 
115        checkNumnberOfEnginesNeededHPT(); // check train engine requirements
116        showCarsNotRoutable(); // list cars that couldn't be routed
117        finshBuildReport(); // number of warnings, build time
118
119        getBuildReport().flush();
120        getBuildReport().close();
121
122        createManifests(); // now make Manifests
123
124        // notify locations have been modified by this train's build
125        for (Location location : _modifiedLocations) {
126            location.setStatus(Location.MODIFIED);
127        }
128
129        // operations automations use wait for train built to create custom
130        // manifests and switch lists
131        getTrain().setPrinted(false);
132        getTrain().setSwitchListStatus(Train.UNKNOWN);
133        getTrain().setCurrentLocation(getTrain().getTrainDepartsRouteLocation());
134        getTrain().setBuilt(true);
135        // create and place train icon
136        getTrain().moveTrainIcon(getTrain().getTrainDepartsRouteLocation());
137
138        log.debug("Done building train ({})", getTrain().getName());
139        showWarningMessage();
140    }
141
142    /**
143     * Figures out if the train terminates into staging, and if true, sets the
144     * termination track. Note if the train is returning back to the same track
145     * in staging getTerminateStagingTrack() is null, and is loaded later when
146     * the departure track is determined.
147     * 
148     * @throws BuildFailedException if staging track can't be found
149     */
150    private void determineIfTrainTerminatesIntoStaging() throws BuildFailedException {
151        // does train terminate into staging?
152        setTerminateStagingTrack(null);
153        List<Track> stagingTracksTerminate = getTerminateLocation().getTracksByMoves(Track.STAGING);
154        if (stagingTracksTerminate.size() > 0) {
155            addLine(THREE, BLANK_LINE);
156            addLine(ONE, Bundle.getMessage("buildTerminateStaging", getTerminateLocation().getName(),
157                    Integer.toString(stagingTracksTerminate.size())));
158            if (stagingTracksTerminate.size() > 1 && Setup.isStagingPromptToEnabled()) {
159                setTerminateStagingTrack(promptToStagingDialog());
160                setStartTime(new Date()); // reset build time since user can take
161                                          // awhile to pick
162            } else {
163                // is this train returning to the same staging in aggressive
164                // mode?
165                if (getDepartureLocation() == getTerminateLocation() &&
166                        Setup.isBuildAggressive() &&
167                        Setup.isStagingTrackImmediatelyAvail()) {
168                    addLine(ONE, Bundle.getMessage("buildStagingReturn", getTerminateLocation().getName()));
169                } else {
170                    for (Track track : stagingTracksTerminate) {
171                        if (checkTerminateStagingTrack(track)) {
172                            setTerminateStagingTrack(track);
173                            addLine(ONE, Bundle.getMessage("buildStagingAvail",
174                                    getTerminateStagingTrack().getName(), getTerminateLocation().getName()));
175                            break;
176                        }
177                    }
178                }
179            }
180            if (getTerminateStagingTrack() == null) {
181                // is this train returning to the same staging in aggressive
182                // mode?
183                if (getDepartureLocation() == getTerminateLocation() &&
184                        Setup.isBuildAggressive() &&
185                        Setup.isStagingTrackImmediatelyAvail()) {
186                    log.debug("Train is returning to same track in staging");
187                } else {
188                    addLine(ONE, Bundle.getMessage("buildErrorStagingFullNote"));
189                    throw new BuildFailedException(
190                            Bundle.getMessage("buildErrorStagingFull", getTerminateLocation().getName()));
191                }
192            }
193        }
194    }
195
196    /**
197     * Figures out if the train is departing staging, and if true, sets the
198     * departure track. Also sets the arrival track if the train is returning to
199     * the same departure track in staging.
200     * 
201     * @throws BuildFailedException if staging departure track not found
202     */
203    private void determineIfTrainDepartsStagingAndAddEngines() throws BuildFailedException {
204        // allow up to two engine and caboose swaps in the train's route
205        RouteLocation engineTerminatesFirstLeg = getTrain().getTrainTerminatesRouteLocation();
206
207        // Adjust where the locos will terminate
208        if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
209                getTrain().getSecondLegStartRouteLocation() != null) {
210            engineTerminatesFirstLeg = getTrain().getSecondLegStartRouteLocation();
211        } else if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
212                getTrain().getThirdLegStartRouteLocation() != null) {
213            engineTerminatesFirstLeg = getTrain().getThirdLegStartRouteLocation();
214        }
215
216        // determine if train is departing staging
217        List<Track> stagingTracks = getDepartureLocation().getTracksByMoves(Track.STAGING);
218        if (stagingTracks.size() > 0) {
219            addLine(THREE, BLANK_LINE);
220            addLine(ONE, Bundle.getMessage("buildDepartStaging", getDepartureLocation().getName(),
221                    Integer.toString(stagingTracks.size())));
222            if (stagingTracks.size() > 1 && Setup.isStagingPromptFromEnabled()) {
223                setDepartureStagingTrack(promptFromStagingDialog());
224                setStartTime(new Date()); // restart build timer
225                if (getDepartureStagingTrack() == null) {
226                    showTrainRequirements();
227                    throw new BuildFailedException(
228                            Bundle.getMessage("buildErrorStagingEmpty", getDepartureLocation().getName()));
229                }
230            } else {
231                for (Track track : stagingTracks) {
232                    // is the departure track available?
233                    if (!checkDepartureStagingTrack(track)) {
234                        addLine(SEVEN,
235                                Bundle.getMessage("buildStagingTrackRestriction", track.getName(),
236                                        getTrain().getName()));
237                        continue;
238                    }
239                    setDepartureStagingTrack(track);
240                    // try each departure track for the required engines
241                    if (getEngines(getTrain().getNumberEngines(), getTrain().getEngineModel(),
242                            getTrain().getEngineRoad(),
243                            getTrain().getTrainDepartsRouteLocation(), engineTerminatesFirstLeg)) {
244                        addLine(SEVEN, Bundle.getMessage("buildDoneAssignEnginesStaging"));
245                        break; // done!
246                    }
247                    setDepartureStagingTrack(null);
248                }
249            }
250            if (getDepartureStagingTrack() == null) {
251                showTrainRequirements();
252                throw new BuildFailedException(
253                        Bundle.getMessage("buildErrorStagingEmpty", getDepartureLocation().getName()));
254            }
255        }
256        getTrain().setTerminationTrack(getTerminateStagingTrack());
257        getTrain().setDepartureTrack(getDepartureStagingTrack());
258    }
259
260    /**
261     * Adds and removes cabooses or car with FRED in the train's route. Up to 2
262     * caboose changes.
263     * 
264     * @throws BuildFailedException
265     */
266    private void addCabooseOrFredToTrain() throws BuildFailedException {
267        // allow up to two caboose swaps in the train's route
268        RouteLocation cabooseOrFredTerminatesFirstLeg = getTrain().getTrainTerminatesRouteLocation();
269        RouteLocation cabooseOrFredTerminatesSecondLeg = getTrain().getTrainTerminatesRouteLocation();
270
271        // determine if there are any caboose changes
272        if ((getTrain().getSecondLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
273                (getTrain().getSecondLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
274            cabooseOrFredTerminatesFirstLeg = getTrain().getSecondLegStartRouteLocation();
275        } else if ((getTrain().getThirdLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
276                (getTrain().getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
277            cabooseOrFredTerminatesFirstLeg = getTrain().getThirdLegStartRouteLocation();
278        }
279        if ((getTrain().getThirdLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
280                (getTrain().getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
281            cabooseOrFredTerminatesSecondLeg = getTrain().getThirdLegStartRouteLocation();
282        }
283
284        // Do caboose changes in reverse order in case there isn't enough track
285        // space second caboose change?
286        if ((getTrain().getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE &&
287                getTrain().getThirdLegStartRouteLocation() != null &&
288                getTrain().getTrainTerminatesRouteLocation() != null) {
289            getCaboose(getTrain().getThirdLegCabooseRoad(), _thirdLeadEngine,
290                    getTrain().getThirdLegStartRouteLocation(),
291                    getTrain().getTrainTerminatesRouteLocation(), true);
292        }
293
294        // first caboose change?
295        if ((getTrain().getSecondLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE &&
296                getTrain().getSecondLegStartRouteLocation() != null &&
297                cabooseOrFredTerminatesSecondLeg != null) {
298            getCaboose(getTrain().getSecondLegCabooseRoad(), _secondLeadEngine,
299                    getTrain().getSecondLegStartRouteLocation(),
300                    cabooseOrFredTerminatesSecondLeg, true);
301        }
302
303        // departure caboose or car with FRED
304        getCaboose(getTrain().getCabooseRoad(), getTrain().getLeadEngine(), getTrain().getTrainDepartsRouteLocation(),
305                cabooseOrFredTerminatesFirstLeg, getTrain().isCabooseNeeded());
306        getCarWithFred(getTrain().getCabooseRoad(), getTrain().getTrainDepartsRouteLocation(),
307                cabooseOrFredTerminatesFirstLeg);
308    }
309
310    /*
311     * User can manually assigns cars that don't have a train, destination or
312     * final destination. Build items can request car type, road, load, route
313     * location, track, and pick up day. Build items can also provide a final
314     * destination, which allows for routing if needed.
315     */
316    private void manualBuild() throws BuildFailedException {
317        // determine if there's a manual build available
318        TrainManualBuildManager manualBuildManager = InstanceManager.getDefault(TrainManualBuildManager.class);
319        TrainManualBuild manualBuild = manualBuildManager.getManualBuildByTrainId(getTrain().getId());
320        if (manualBuild == null) {
321            return;
322        }
323        addLine(ONE, BLANK_LINE);
324        addLine(ONE, Bundle.getMessage("mbuildFound", getTrain().getName()));
325        for (TrainManualBuildItem mbi : manualBuild.getItemsBySequenceList()) {
326            addLine(THREE,
327                    Bundle.getMessage("mbuildRequest", mbi.getId(), mbi.getTypeName(), mbi.getRoadName(),
328                            mbi.getLoadName(), mbi.getLocationName(), mbi.getLocationTrackName(),
329                            mbi.getDestinationName(), mbi.getDestinationTrackName(), mbi.getTrainScheduleName(),
330                            mbi.getCount()));
331            // the number of cars requested by this line item
332            int count = mbi.getCount();
333            if (count > 0) {
334                for (_carIndex = 0; _carIndex < getCarList().size(); _carIndex++) {
335                    Car car = getCarList().get(_carIndex);
336                    // find a car that meets the user's requests
337                    if (!checkItem(mbi, car)) {
338                        continue;
339                    }
340                    addLine(FIVE,
341                            Bundle.getMessage("mbuildFoundCar", car.getTypeName(), car.getTypeExtensions(),
342                                    car.getRoadName(), car.getLoadName(), car.getLocationName(), car.getTrackName()));
343                    // there could be a an optional destination
344                    car.setFinalDestination(mbi.getDestination());
345                    car.setFinalDestinationTrack(mbi.getDestinationTrack());
346                    boolean carAdded = false;
347                    // case where the user specified a route location
348                    if (mbi.getRouteLocation() != null) {
349                        carAdded = findDestinationForCar(mbi.getRouteLocation(), car);
350                    } else {
351                        for (RouteLocation rl : getRouteList()) {
352                            // start looking at car's location
353                            if (rl.getLocation() == car.getLocation()) {
354                                carAdded = findDestinationForCar(rl, car);
355                            }
356                            if (carAdded) {
357                                break; // done
358                            }
359                        }
360                    }
361                    // if not assigned to train, clear final destination
362                    if (!carAdded) {
363                        car.setFinalDestination(null);
364                        car.setFinalDestinationTrack(null);
365                    } else {
366                        if (--count <= 0) {
367                            break; // done
368                        }
369                    }
370                }
371            }
372
373            addLine(THREE, Bundle.getMessage("mbuildCompleted", mbi.getId(), manualBuild.getTrainName(),
374                    mbi.getCount() - count, mbi.getCount()));
375            addLine(THREE, BLANK_LINE);
376
377            if (count > 0 && mbi.isFailEnabled()) {
378                throw new BuildFailedException(
379                        Bundle.getMessage("mbuildFail", manualBuild.getTrainName(), mbi.getId()));
380            } else if (count > 0 && mbi.isWarnEnabled()) {
381                _warnings++;
382                addLine(ONE, Bundle.getMessage("mbuildWarn", manualBuild.getTrainName(), mbi.getId()));
383                addLine(ONE, BLANK_LINE);
384            }
385
386            if (mbi.isRemoveEnabled()) {
387                for (_carIndex = 0; _carIndex < getCarList().size(); _carIndex++) {
388                    Car car = getCarList().get(_carIndex);
389                    // find a car that meets the user's requests
390                    if (checkItem(mbi, car)) {
391                        addLine(FIVE, Bundle.getMessage("mbuildRemove", mbi.getId(), car.toString(), car.getTrackTypeName(),
392                                car.getLocationName(), car.getTrackName()));
393                        remove(car);
394                    }
395                }
396                addLine(FIVE, BLANK_LINE);
397            }
398        }
399        addLine(ONE, Bundle.getMessage("mbuildDone"));
400    }
401
402    /*
403     * Returns true if the car matches the user's request. Car can not be
404     * assigned to this train, have a destination, or a final destination.
405     */
406    private boolean checkItem(TrainManualBuildItem mbi, Car car) {
407        if (car.getTrain() != null) {
408            log.debug("Car ({}) is assigned to train ({})", car.toString(), car.getTrain().getName());
409            return false;
410        }
411        if (car.getDestination() != null) {
412            log.debug("Car ({}) has destination ({})", car.toString(), car.getDestination().getName());
413            return false;
414        }
415        if (car.getFinalDestination() != null) {
416            log.debug("Car ({}) has final destination ({})", car.toString(),
417                    car.getFinalDestination().getName());
418            return false;
419        }
420        if (!mbi.getTypeName().equals(Car.NONE) && !car.getTypeName().equals(mbi.getTypeName())) {
421            return false;
422        }
423        if (!mbi.getRoadName().equals(Car.NONE) && !car.getRoadName().equals(mbi.getRoadName())) {
424            return false;
425        }
426        if (!mbi.getLoadName().equals(Car.NONE) && !car.getLoadName().equals(mbi.getLoadName())) {
427            return false;
428        }
429        if (mbi.getRouteLocation() != null && car.getLocation() != mbi.getRouteLocation().getLocation()) {
430            return false;
431        }
432        if (mbi.getLocationTrack() != null && car.getTrack() != mbi.getLocationTrack()) {
433            return false;
434        }
435        // pick up day
436        if (trainScheduleManager.getActiveSchedule() != null &&
437                !mbi.getTrainScheduleName().equals(TrainManualBuildItem.NONE) &&
438                !trainScheduleManager.getActiveSchedule().getId().equals(mbi.getTrainScheduleId())) {
439            return false;
440        }
441        return true;
442    }
443
444    private boolean findDestinationForCar(RouteLocation rl, Car car) throws BuildFailedException {
445        if (!checkRouteLocationMoves(rl, car)) {
446            return false;
447        }
448        if (!checkRouteLocation(rl)) {
449            addLine(ONE, BLANK_LINE);
450            return false;
451        }
452        // if car is sent to quick service track, moves for car is bumped
453        int moves = car.getMoves();
454        findDestinationsFromLocation(rl, car, false);
455        if (car.getTrain() == null && moves == car.getMoves()) {
456            return false;
457        }
458        return true;
459    }
460
461    /*
462     * returns true if there are moves available at the car's departure route
463     * location
464     */
465    private boolean checkRouteLocationMoves(RouteLocation rl, Car car) {
466        if (rl.getCarMoves() < rl.getMaxCarMoves()) {
467            return true;
468        }
469        addLine(FIVE, Bundle.getMessage("mbuildRouteLocationMoves", rl.getName(), rl.getId(), rl.getMaxCarMoves(),
470                rl.getMaxCarMoves() - rl.getCarMoves()));
471        addLine(FIVE, Bundle.getMessage("buildNoDestForCar", car.toString()));
472        addLine(FIVE, BLANK_LINE);
473        return false;
474    }
475
476    /**
477     * Routine to find and add available cars to the train. In normal mode
478     * performs a single pass. In aggressive mode, will perform multiple passes.
479     * If train is departing staging and in aggressive mode, will try again
480     * using normal mode if there's a train build issue.
481     * 
482     * @throws BuildFailedException
483     */
484    private void addCarsToTrain() throws BuildFailedException {
485        addLine(THREE, BLANK_LINE);
486        addLine(THREE,
487                Bundle.getMessage("buildTrain", getTrain().getNumberCarsRequested(), getTrain().getName(),
488                        getCarList().size()));
489
490        if (Setup.isBuildAggressive() && !getTrain().isBuildTrainNormalEnabled()) {
491            // perform a multiple pass build for this train, default is two
492            // passes
493            int pass = 0;
494            while (pass++ < Setup.getNumberPasses()) {
495                addCarsToTrain(pass, false);
496            }
497            // are cars stuck in staging?
498            secondAttemptNormalBuild();
499        } else {
500            addCarsToTrain(Setup.getNumberPasses(), true); // normal build one
501                                                           // pass
502        }
503    }
504
505    /**
506     * If cars stuck in staging, try building again in normal mode.
507     * 
508     * @throws BuildFailedException
509     */
510    private void secondAttemptNormalBuild() throws BuildFailedException {
511        if (Setup.isStagingTryNormalBuildEnabled() && isCarStuckStaging()) {
512            addLine(ONE, Bundle.getMessage("buildFailedTryNormalMode"));
513            addLine(ONE, BLANK_LINE);
514            getTrain().reset();
515            getTrain().setStatusCode(Train.CODE_BUILDING);
516            getTrain().setLeadEngine(null);
517            // using the same departure and termination tracks
518            getTrain().setDepartureTrack(getDepartureStagingTrack());
519            getTrain().setTerminationTrack(getTerminateStagingTrack());
520            showAndInitializeTrainRoute();
521            getAndRemoveEnginesFromList();
522            addEnginesToTrain();
523            createCarList();
524            adjustCarsInStaging();
525            showCarsByLocation();
526            addCabooseOrFredToTrain();
527            removeCaboosesAndCarsWithFred();
528            saveCarFinalDestinations(); // save final destination and schedule
529                                        // id
530            blockCarsFromStaging(); // block cars from staging
531            addCarsToTrain(Setup.getNumberPasses(), true); // try normal build
532                                                           // one pass
533        }
534    }
535
536    /**
537     * Main routine to place cars into the train. Can be called multiple times.
538     * When departing staging, ignore staged cars on the first pass unless the
539     * option to build normal was selected by user.
540     *
541     * @param pass   Which pass when there are multiple passes requested by
542     *               user.
543     * @param normal True if single pass or normal mode is requested by user.
544     * @throws BuildFailedException
545     */
546    private void addCarsToTrain(int pass, boolean normal) throws BuildFailedException {
547        addLine(THREE, BLANK_LINE);
548        if (normal) {
549            addLine(THREE, Bundle.getMessage("NormalModeWhenBuilding"));
550        } else {
551            addLine(THREE, Bundle.getMessage("buildMultiplePass", pass, Setup.getNumberPasses()));
552        }
553        // now go through each location starting at departure and place cars as
554        // requested
555        for (RouteLocation rl : getRouteList()) {
556            if (!checkRouteLocation(rl)) {
557                continue;
558            }
559
560            _completedMoves = 0; // moves completed for this location
561            _reqNumOfMoves = rl.getMaxCarMoves() - rl.getCarMoves();
562
563            if (!normal) {
564                if (rl == getTrain().getTrainDepartsRouteLocation()) {
565                    _reqNumOfMoves = (rl.getMaxCarMoves() - rl.getCarMoves()) * pass / Setup.getNumberPasses();
566                } else if (pass == 1) {
567                    _reqNumOfMoves = (rl.getMaxCarMoves() - rl.getCarMoves()) / 2;
568                    // round up requested moves
569                    int remainder = (rl.getMaxCarMoves() - rl.getCarMoves()) % 2;
570                    if (remainder > 0) {
571                        _reqNumOfMoves++;
572                    }
573                }
574            }
575
576            // if departing staging make adjustments
577            if (rl == getTrain().getTrainDepartsRouteLocation()) {
578                if (pass == 1) {
579                    makeAdjustmentsIfDepartingStaging();
580                } else {
581                    restoreCarsIfDepartingStaging();
582                }
583            }
584
585            int saveReqMoves = _reqNumOfMoves; // save a copy for status message
586            addLine(ONE,
587                    Bundle.getMessage("buildLocReqMoves", rl.getName(), rl.getId(), _reqNumOfMoves,
588                            rl.getMaxCarMoves() - rl.getCarMoves(), rl.getMaxCarMoves()));
589            addLine(FIVE, BLANK_LINE);
590
591            // show the car load generation options for staging
592            if (rl == getTrain().getTrainDepartsRouteLocation()) {
593                showLoadGenerationOptionsStaging();
594            }
595
596            _carIndex = 0; // see reportCarsNotMoved(rl) below
597
598            findDestinationsForCarsFromLocation(rl, false); // first pass
599
600            // perform 2nd pass if aggressive mode and there are requested
601            // moves. This will perform local moves at this location, services
602            // off spot tracks, only in aggressive mode and at least one car
603            // has a new destination
604            if (Setup.isBuildAggressive() && saveReqMoves != _reqNumOfMoves) {
605                log.debug("Perform extra pass at location ({})", rl.getName());
606                // use up to half of the available moves left for this location
607                if (_reqNumOfMoves < (rl.getMaxCarMoves() - rl.getCarMoves()) / 2) {
608                    _reqNumOfMoves = (rl.getMaxCarMoves() - rl.getCarMoves()) / 2;
609                }
610                findDestinationsForCarsFromLocation(rl, true); // second pass
611
612                // we might have freed up space at a spur that has an alternate
613                // track
614                if (redirectCarsFromAlternateTrack()) {
615                    addLine(SEVEN, BLANK_LINE);
616                }
617            }
618            if (rl == getTrain().getTrainDepartsRouteLocation() &&
619                    pass == Setup.getNumberPasses() &&
620                    isCarStuckStaging()) {
621                return; // report ASAP that there are stuck cars
622            }
623            addLine(ONE,
624                    Bundle.getMessage("buildStatusMsg",
625                            (saveReqMoves <= _completedMoves ? Bundle.getMessage("Success")
626                                    : Bundle.getMessage("Partial")),
627                            Integer.toString(_completedMoves), Integer.toString(saveReqMoves), rl.getName(),
628                            getTrain().getName()));
629
630            if (_reqNumOfMoves <= 0 && pass == Setup.getNumberPasses()) {
631                showCarsNotMoved(rl);
632            }
633        }
634    }
635
636    private void setTrainBuildStatus() {
637        if (_numberCars < getTrain().getNumberCarsRequested()) {
638            getTrain().setStatusCode(Train.CODE_PARTIAL_BUILT);
639            addLine(ONE,
640                    Train.PARTIAL_BUILT +
641                            " " +
642                            getTrain().getNumberCarsWorked() +
643                            "/" +
644                            getTrain().getNumberCarsRequested() +
645                            " " +
646                            Bundle.getMessage("cars"));
647        } else {
648            getTrain().setStatusCode(Train.CODE_BUILT);
649            addLine(ONE,
650                    Train.BUILT + " " + getTrain().getNumberCarsWorked() + " " + Bundle.getMessage("cars"));
651        }
652    }
653
654    private void createManifests() throws BuildFailedException {
655        new TrainManifest(getTrain());
656        try {
657            new JsonManifest(getTrain()).build();
658        } catch (
659                IOException |
660                RuntimeException ex) {
661            log.error("Unable to create JSON manifest: {}", ex.getLocalizedMessage());
662            log.error("JSON manifest stack trace:", ex);
663            throw new BuildFailedException(ex);
664        }
665        new TrainCsvManifest(getTrain());
666    }
667
668    private void showWarningMessage() {
669        if (trainManager.isBuildMessagesEnabled() && _warnings > 0) {
670            JmriJOptionPane.showMessageDialog(null,
671                    Bundle.getMessage("buildCheckReport", getTrain().getName(), getTrain().getDescription()),
672                    Bundle.getMessage("buildWarningMsg", getTrain().getName(), _warnings),
673                    JmriJOptionPane.WARNING_MESSAGE);
674        }
675    }
676
677    private void buildFailed(BuildFailedException e) {
678        String msg = e.getMessage();
679        getTrain().setBuildFailedMessage(msg);
680        getTrain().setBuildFailed(true);
681        log.debug(msg);
682
683        if (trainManager.isBuildMessagesEnabled()) {
684            // don't pass the object getTrain() to the GUI, can cause thread lock
685            String trainName = getTrain().getName();
686            String trainDescription = getTrain().getDescription();
687            if (e.getExceptionType().equals(BuildFailedException.NORMAL)) {
688                JmriJOptionPane.showMessageDialog(null, msg,
689                        Bundle.getMessage("buildErrorMsg", trainName, trainDescription), JmriJOptionPane.ERROR_MESSAGE);
690            } else {
691                // build error, could not find destinations for cars departing
692                // staging
693                Object[] options = {Bundle.getMessage("buttonRemoveCars"), Bundle.getMessage("ButtonOK")};
694                int results = JmriJOptionPane.showOptionDialog(null, msg,
695                        Bundle.getMessage("buildErrorMsg", trainName, trainDescription),
696                        JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.ERROR_MESSAGE, null, options, options[1]);
697                if (results == 0) {
698                    log.debug("User requested that cars be removed from staging track");
699                    removeCarsFromStaging();
700                }
701            }
702            int size = carManager.getList(getTrain()).size();
703            if (size > 0) {
704                if (JmriJOptionPane.showConfirmDialog(null,
705                        Bundle.getMessage("buildCarsResetTrain", size, trainName),
706                        Bundle.getMessage("buildResetTrain"),
707                        JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.YES_OPTION) {
708                    getTrain().setStatusCode(Train.CODE_TRAIN_RESET);
709                }
710            } else if ((size = engineManager.getList(getTrain()).size()) > 0) {
711                if (JmriJOptionPane.showConfirmDialog(null,
712                        Bundle.getMessage("buildEnginesResetTrain", size, trainName),
713                        Bundle.getMessage("buildResetTrain"),
714                        JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.YES_OPTION) {
715                    getTrain().setStatusCode(Train.CODE_TRAIN_RESET);
716                }
717            }
718        } else {
719            // build messages disabled
720            // remove cars and engines from this train via property change
721            getTrain().setStatusCode(Train.CODE_TRAIN_RESET);
722        }
723
724        getTrain().setStatusCode(Train.CODE_BUILD_FAILED);
725
726        if (getBuildReport() != null) {
727            addLine(ONE, msg);
728            // Write to disk and close buildReport
729            addLine(ONE,
730                    Bundle.getMessage("buildFailedMsg", getTrain().getName()));
731            getBuildReport().flush();
732            getBuildReport().close();
733        }
734    }
735
736    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrainBuilder.class);
737
738}