001package jmri.jmrit.operations.trains.trainbuilder;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import org.apache.commons.lang3.StringUtils;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import jmri.jmrit.operations.locations.Location;
011import jmri.jmrit.operations.locations.Track;
012import jmri.jmrit.operations.rollingstock.RollingStock;
013import jmri.jmrit.operations.rollingstock.engines.Engine;
014import jmri.jmrit.operations.routes.Route;
015import jmri.jmrit.operations.routes.RouteLocation;
016import jmri.jmrit.operations.setup.Control;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.jmrit.operations.trains.BuildFailedException;
019import jmri.jmrit.operations.trains.Train;
020
021/**
022 * Contains methods for engines when building a train.
023 * 
024 * @author Daniel Boudreau Copyright (C) 2022
025 */
026public class TrainBuilderEngines extends TrainBuilderBase {
027
028    /**
029     * Builds a list of possible engines for this train.
030     */
031    protected void getAndRemoveEnginesFromList() {
032        setEngineList(engineManager.getAvailableTrainList(getTrain()));
033
034        // remove any locos that the train can't use
035        for (int indexEng = 0; indexEng < getEngineList().size(); indexEng++) {
036            Engine engine = getEngineList().get(indexEng);
037            // remove engines types that train does not service
038            if (!getTrain().isTypeNameAccepted(engine.getTypeName())) {
039                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineType", engine.toString(),
040                        engine.getLocationName(), engine.getTrackName(), engine.getTypeName()));
041                getEngineList().remove(indexEng--);
042                continue;
043            }
044            // remove engines with roads that train does not service
045            if (!getTrain().isLocoRoadNameAccepted(engine.getRoadName())) {
046                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineRoad", engine.toString(),
047                        engine.getLocationName(), engine.getTrackName(), engine.getRoadName()));
048                getEngineList().remove(indexEng--);
049                continue;
050            }
051            // remove engines with owners that train does not service
052            if (!getTrain().isOwnerNameAccepted(engine.getOwnerName())) {
053                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineOwner", engine.toString(),
054                        engine.getLocationName(), engine.getTrackName(), engine.getOwnerName()));
055                getEngineList().remove(indexEng--);
056                continue;
057            }
058            // remove engines with built dates that train does not service
059            if (!getTrain().isBuiltDateAccepted(engine.getBuilt())) {
060                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineBuilt", engine.toString(),
061                        engine.getLocationName(), engine.getTrackName(), engine.getBuilt()));
062                getEngineList().remove(indexEng--);
063                continue;
064            }
065            // remove engines that are out of service
066            if (engine.isOutOfService()) {
067                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineOutOfService", engine.toString(),
068                        engine.getLocationName(), engine.getTrackName()));
069                getEngineList().remove(indexEng--);
070                continue;
071            }
072            // remove engines that aren't on the train's route
073            if (getTrain().getRoute().getLastLocationByName(engine.getLocationName()) == null) {
074                log.debug("removing engine ({}) location ({}) not serviced by train", engine.toString(),
075                        engine.getLocationName());
076                getEngineList().remove(indexEng--);
077                continue;
078            }
079            // is engine at interchange?
080            if (engine.getTrack().isInterchange()) {
081                // don't service a engine at interchange and has been dropped off
082                // by this train
083                if (engine.getTrack().getPickupOption().equals(Track.ANY) &&
084                        engine.getLastRouteId().equals(getTrain().getRoute().getId())) {
085                    addLine(SEVEN, Bundle.getMessage("buildExcludeEngineDropByTrain", engine.toString(),
086                            engine.getTypeName(), getTrain().getRoute().getName(), engine.getLocationName(),
087                            engine.getTrackName()));
088                    getEngineList().remove(indexEng--);
089                    continue;
090                }
091            }
092            // is engine at interchange or spur and is this train allowed to pull?
093            if (engine.getTrack().isInterchange() || engine.getTrack().isSpur()) {
094                if (engine.getTrack().getPickupOption().equals(Track.TRAINS) ||
095                        engine.getTrack().getPickupOption().equals(Track.EXCLUDE_TRAINS)) {
096                    if (engine.getTrack().isPickupTrainAccepted(getTrain())) {
097                        log.debug("Engine ({}) can be picked up by this train", engine.toString());
098                    } else {
099                        addLine(SEVEN,
100                                Bundle.getMessage("buildExcludeEngineByTrain", engine.toString(), engine.getTypeName(),
101                                        engine.getTrack().getTrackTypeName(), engine.getLocationName(),
102                                        engine.getTrackName()));
103                        getEngineList().remove(indexEng--);
104                        continue;
105                    }
106                } else if (engine.getTrack().getPickupOption().equals(Track.ROUTES) ||
107                        engine.getTrack().getPickupOption().equals(Track.EXCLUDE_ROUTES)) {
108                    if (engine.getTrack().isPickupRouteAccepted(getTrain().getRoute())) {
109                        log.debug("Engine ({}) can be picked up by this route", engine.toString());
110                    } else {
111                        addLine(SEVEN,
112                                Bundle.getMessage("buildExcludeEngineByRoute", engine.toString(), engine.getTypeName(),
113                                        engine.getTrack().getTrackTypeName(), engine.getLocationName(),
114                                        engine.getTrackName()));
115                        getEngineList().remove(indexEng--);
116                        continue;
117                    }
118                }
119            }
120        }
121    }
122
123    protected boolean getEngines(String requestedEngines, String model, String road, RouteLocation rl,
124            RouteLocation rld) throws BuildFailedException {
125        return getEngines(requestedEngines, model, road, rl, rld, !USE_BUNIT);
126    }
127
128    /**
129     * Get the engines for this train at a route location. If departing from
130     * staging engines must come from that track. Finds the required number of
131     * engines in a consist, or if the option to build from single locos, builds
132     * a consist for the user. When true, engines successfully added to train
133     * for the leg requested.
134     *
135     * @param requestedEngines Requested number of Engines, can be number, AUTO
136     *                         or AUTO HPT
137     * @param model            Optional model name for the engines
138     * @param road             Optional road name for the engines
139     * @param rl               Departure route location for the engines
140     * @param rld              Destination route location for the engines
141     * @param useBunit         true if B unit engine is allowed
142     * @return true if correct number of engines found.
143     * @throws BuildFailedException if coding issue
144     */
145    protected boolean getEngines(String requestedEngines, String model, String road, RouteLocation rl,
146            RouteLocation rld, boolean useBunit) throws BuildFailedException {
147        // load departure track if staging
148        Track departStagingTrack = null;
149        if (rl == getTrain().getTrainDepartsRouteLocation()) {
150            // get departure track from staging, could be null
151            departStagingTrack = getDepartureStagingTrack();
152        }
153
154        int reqNumberEngines = getNumberEngines(requestedEngines);
155
156        // if not departing staging track and engines aren't required done!
157        if (departStagingTrack == null && reqNumberEngines == 0) {
158            return true;
159        }
160        // if departing staging and no engines required and none available,
161        // we're done
162        if (departStagingTrack != null && reqNumberEngines == 0 && departStagingTrack.getNumberEngines() == 0) {
163            return true;
164        }
165
166        // code check, staging track selection checks number of engines needed
167        if (departStagingTrack != null &&
168                reqNumberEngines != 0 &&
169                departStagingTrack.getNumberEngines() != reqNumberEngines) {
170            throw new BuildFailedException(Bundle.getMessage("buildStagingNotEngines", departStagingTrack.getName(),
171                    departStagingTrack.getNumberEngines(), reqNumberEngines));
172        }
173
174        // code check
175        if (rl == null || rld == null) {
176            throw new BuildFailedException(
177                    Bundle.getMessage("buildErrorEngLocUnknown"));
178        }
179
180        addLine(FIVE, Bundle.getMessage("buildBegineSearchEngines", reqNumberEngines, model, road,
181                rl.getName(), rld.getName()));
182
183        int assignedLocos = 0; // the number of locos assigned to this train
184        List<Engine> singleLocos = new ArrayList<>();
185        for (int indexEng = 0; indexEng < getEngineList().size(); indexEng++) {
186            Engine engine = getEngineList().get(indexEng);
187            log.debug("Engine ({}) at location ({}, {})", engine.toString(), engine.getLocationName(),
188                    engine.getTrackName());
189
190            // use engines that are departing from the selected staging track
191            // (departTrack
192            // != null if staging)
193            if (departStagingTrack != null && !departStagingTrack.equals(engine.getTrack())) {
194                continue;
195            }
196            // use engines that are departing from the correct location
197            if (!engine.getLocationName().equals(rl.getName())) {
198                log.debug("Skipping engine ({}) at location ({})", engine.toString(), engine.getLocationName());
199                continue;
200            }
201            // skip engines models that train does not service
202            if (!model.equals(Train.NONE) && !engine.getModel().equals(model)) {
203                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineModel", engine.toString(),
204                        engine.getModel(), engine.getLocationName()));
205                continue;
206            }
207            // Does the train have a very specific engine road name requirement?
208            if (!road.equals(Train.NONE) && !engine.getRoadName().equals(road)) {
209                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineRoad", engine.toString(),
210                        engine.getLocationName(), engine.getTrackName(), engine.getRoadName()));
211                continue;
212            }
213            // skip engines on tracks that don't service the train's departure
214            // direction
215            if (!checkPickUpTrainDirection(engine, rl)) {
216                continue;
217            }
218            // skip engines that have been assigned destinations that don't
219            // match the requested destination
220            if (engine.getDestination() != null && !engine.getDestinationName().equals(rld.getName())) {
221                addLine(SEVEN, Bundle.getMessage("buildExcludeEngineDestination", engine.toString(),
222                        engine.getDestinationName()));
223                continue;
224            }
225            // don't use non lead locos in a consist
226            if (engine.getConsist() != null) {
227                if (engine.isLead()) {
228                    addLine(SEVEN,
229                            Bundle.getMessage("buildEngineLeadConsist", engine.toString(),
230                                    engine.getConsist().getName(), engine.getConsist().getEngines().size()));
231                } else {
232                    continue;
233                }
234            }
235            if (!checkQuickServiceDeparting(engine, rl)) {
236                continue;
237            }
238            // departing staging, then all locos must go!
239            if (departStagingTrack != null) {
240                if (!setEngineDestination(engine, rl, rld)) {
241                    return false;
242                }
243                getEngineList().remove(indexEng--);
244                if (engine.getConsist() != null) {
245                    assignedLocos = assignedLocos + engine.getConsist().getSize();
246                } else {
247                    assignedLocos++;
248                }
249                continue;
250            }
251            // can't use B units if requesting one loco
252            if (!useBunit && reqNumberEngines == 1 && engine.isBunit()) {
253                addLine(SEVEN,
254                        Bundle.getMessage("buildExcludeEngineBunit", engine.toString(), engine.getModel()));
255                continue;
256            }
257            // is this engine part of a consist?
258            if (engine.getConsist() == null) {
259                // single engine, but does the train require a consist?
260                if (reqNumberEngines > 1) {
261                    addLine(SEVEN,
262                            Bundle.getMessage("buildExcludeEngineSingle", engine.toString(), reqNumberEngines));
263                    singleLocos.add(engine);
264                    continue;
265                }
266                // engine is part of a consist
267            } else if (engine.getConsist().getSize() == reqNumberEngines) {
268                log.debug("Consist ({}) has the required number of engines", engine.getConsist().getName()); // NOI18N
269            } else if (reqNumberEngines != 0) {
270                addLine(SEVEN,
271                        Bundle.getMessage("buildExcludeEngConsistNumber", engine.toString(),
272                                engine.getConsist().getName(), engine.getConsist().getSize()));
273                continue;
274            }
275            // found a loco or consist!
276            assignedLocos++;
277
278            // now find terminal track for engine(s)
279            addLine(FIVE,
280                    Bundle.getMessage("buildEngineRoadModelType", engine.toString(), engine.getRoadName(),
281                            engine.getModel(), engine.getTypeName(), engine.getLocationName(), engine.getTrackName(),
282                            rld.getName()));
283            if (setEngineDestination(engine, rl, rld)) {
284                getEngineList().remove(indexEng--);
285                return true; // normal exit when not staging
286            }
287        }
288        // build a consist out of non-consisted locos
289        if (assignedLocos == 0 && reqNumberEngines > 1 && getTrain().isBuildConsistEnabled()) {
290            if (buildConsistFromSingleLocos(reqNumberEngines, singleLocos, rl, rld)) {
291                return true; // normal exit when building with single locos
292            }
293        }
294        if (assignedLocos == 0) {
295            String locationName = rl.getName();
296            if (departStagingTrack != null) {
297                locationName = locationName + ", " + departStagingTrack.getName();
298            }
299            addLine(FIVE, Bundle.getMessage("buildNoLocosFoundAtLocation", locationName));
300        } else if (departStagingTrack != null && (reqNumberEngines == 0 || reqNumberEngines == assignedLocos)) {
301            return true; // normal exit assigning from staging
302        }
303        // not able to assign engines to train
304        return false;
305    }
306
307    private boolean buildConsistFromSingleLocos(int reqNumberEngines, List<Engine> singleLocos, RouteLocation rl,
308            RouteLocation rld) {
309        addLine(FIVE, Bundle.getMessage("buildOptionBuildConsist", reqNumberEngines, rl.getName()));
310        addLine(FIVE, Bundle.getMessage("buildOptionSingleLocos", singleLocos.size(), rl.getName()));
311        if (singleLocos.size() >= reqNumberEngines) {
312            int locos = 0;
313            // first find an "A" unit
314            for (Engine engine : singleLocos) {
315                if (engine.isBunit()) {
316                    continue;
317                }
318                if (setEngineDestination(engine, rl, rld)) {
319                    getEngineList().remove(engine);
320                    singleLocos.remove(engine);
321                    locos++;
322                    break; // found "A" unit
323                }
324            }
325            // did we find an "A" unit?
326            if (locos > 0) {
327                // now add the rest "A" or "B" units
328                for (Engine engine : singleLocos) {
329                    if (setEngineDestination(engine, rl, rld)) {
330                        getEngineList().remove(engine);
331                        locos++;
332                    }
333                    if (locos == reqNumberEngines) {
334                        return true; // done!
335                    }
336                }
337            } else {
338                // list the "B" units found
339                for (Engine engine : singleLocos) {
340                    if (engine.isBunit()) {
341                        addLine(FIVE,
342                                Bundle.getMessage("BuildEngineBunit", engine.toString(), engine.getLocationName(),
343                                        engine.getTrackName()));
344                    }
345                }
346            }
347        }
348        return false;
349    }
350
351    /**
352     * Adds engines to the train starting at the first location in the train's
353     * route. Note that engines from staging are already part of the train.
354     * There can be up to two engine swaps in a train's route.
355     * 
356     * @throws BuildFailedException if required engines can't be added to train.
357     */
358    protected void addEnginesToTrain() throws BuildFailedException {
359        // allow up to two engine and caboose swaps in the train's route
360        RouteLocation engineTerminatesFirstLeg = getTrain().getTrainTerminatesRouteLocation();
361        RouteLocation engineTerminatesSecondLeg = getTrain().getTrainTerminatesRouteLocation();
362
363        // Adjust where the locos will terminate
364        if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
365                getTrain().getSecondLegStartRouteLocation() != null) {
366            engineTerminatesFirstLeg = getTrain().getSecondLegStartRouteLocation();
367        }
368        if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
369                getTrain().getThirdLegStartRouteLocation() != null) {
370            engineTerminatesSecondLeg = getTrain().getThirdLegStartRouteLocation();
371            // No engine or caboose change at first leg?
372            if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) != Train.CHANGE_ENGINES) {
373                engineTerminatesFirstLeg = getTrain().getThirdLegStartRouteLocation();
374            }
375        }
376
377        if (getTrain().getLeadEngine() == null) {
378            // option to remove locos from the train
379            if ((getTrain().getSecondLegOptions() & Train.REMOVE_ENGINES) == Train.REMOVE_ENGINES &&
380                    getTrain().getSecondLegStartRouteLocation() != null) {
381                addLine(THREE, BLANK_LINE);
382                addLine(THREE,
383                        Bundle.getMessage("buildTrainRemoveEngines", getTrain().getSecondLegNumberEngines(),
384                                getTrain().getSecondLegStartLocationName(), getTrain().getSecondLegEngineModel(),
385                                getTrain().getSecondLegEngineRoad()));
386                if (getEngines(getTrain().getSecondLegNumberEngines(), getTrain().getSecondLegEngineModel(),
387                        getTrain().getSecondLegEngineRoad(), getTrain().getTrainDepartsRouteLocation(),
388                        getTrain().getSecondLegStartRouteLocation())) {
389                } else if (getConsist(getTrain().getSecondLegNumberEngines(), getTrain().getSecondLegEngineModel(),
390                        getTrain().getSecondLegEngineRoad(), getTrain().getTrainDepartsRouteLocation(),
391                        getTrain().getSecondLegStartRouteLocation())) {
392                } else {
393                    throw new BuildFailedException(Bundle.getMessage("buildErrorEngines",
394                            getTrain().getSecondLegNumberEngines(), getTrain().getTrainDepartsName(),
395                            getTrain().getSecondLegStartRouteLocation().getLocation().getName()));
396                }
397            }
398            if ((getTrain().getThirdLegOptions() & Train.REMOVE_ENGINES) == Train.REMOVE_ENGINES &&
399                    getTrain().getThirdLegStartRouteLocation() != null) {
400                addLine(THREE, BLANK_LINE);
401                addLine(THREE,
402                        Bundle.getMessage("buildTrainRemoveEngines", getTrain().getThirdLegNumberEngines(),
403                                getTrain().getThirdLegStartLocationName(), getTrain().getThirdLegEngineModel(),
404                                getTrain().getThirdLegEngineRoad()));
405                if (getEngines(getTrain().getThirdLegNumberEngines(), getTrain().getThirdLegEngineModel(),
406                        getTrain().getThirdLegEngineRoad(), getTrain().getTrainDepartsRouteLocation(),
407                        getTrain().getThirdLegStartRouteLocation())) {
408                } else if (getConsist(getTrain().getThirdLegNumberEngines(), getTrain().getThirdLegEngineModel(),
409                        getTrain().getThirdLegEngineRoad(), getTrain().getTrainDepartsRouteLocation(),
410                        getTrain().getThirdLegStartRouteLocation())) {
411                } else {
412                    throw new BuildFailedException(Bundle.getMessage("buildErrorEngines",
413                            getTrain().getThirdLegNumberEngines(), getTrain().getTrainDepartsName(),
414                            getTrain().getThirdLegStartRouteLocation().getLocation().getName()));
415                }
416            }
417            // load engines at the start of the route for this train
418            addLine(THREE, BLANK_LINE);
419            if (getEngines(getTrain().getNumberEngines(), getTrain().getEngineModel(), getTrain().getEngineRoad(),
420                    getTrain().getTrainDepartsRouteLocation(), engineTerminatesFirstLeg)) {
421                // when adding a caboose later in the route, no engine change
422                _secondLeadEngine = _lastEngine;
423                _thirdLeadEngine = _lastEngine;
424            } else if (getConsist(getTrain().getNumberEngines(), getTrain().getEngineModel(),
425                    getTrain().getEngineRoad(),
426                    getTrain().getTrainDepartsRouteLocation(), engineTerminatesFirstLeg)) {
427                // when adding a caboose later in the route, no engine change
428                _secondLeadEngine = _lastEngine;
429                _thirdLeadEngine = _lastEngine;
430            } else {
431                throw new BuildFailedException(Bundle.getMessage("buildErrorEngines", getTrain().getNumberEngines(),
432                        getTrain().getTrainDepartsName(), engineTerminatesFirstLeg.getName()));
433            }
434        }
435
436        // First engine change in route?
437        if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES ||
438                (getTrain().getSecondLegOptions() & Train.ADD_ENGINES) == Train.ADD_ENGINES) {
439            addLine(THREE, BLANK_LINE);
440            if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
441                addLine(THREE,
442                        Bundle.getMessage("buildTrainEngineChange", getTrain().getSecondLegStartLocationName(),
443                                getTrain().getSecondLegNumberEngines(), getTrain().getSecondLegEngineModel(),
444                                getTrain().getSecondLegEngineRoad()));
445            } else {
446                addLine(THREE,
447                        Bundle.getMessage("buildTrainAddEngines", getTrain().getSecondLegNumberEngines(),
448                                getTrain().getSecondLegStartLocationName(), getTrain().getSecondLegEngineModel(),
449                                getTrain().getSecondLegEngineRoad()));
450            }
451            if (getEngines(getTrain().getSecondLegNumberEngines(), getTrain().getSecondLegEngineModel(),
452                    getTrain().getSecondLegEngineRoad(), getTrain().getSecondLegStartRouteLocation(),
453                    engineTerminatesSecondLeg)) {
454                _secondLeadEngine = _lastEngine;
455                _thirdLeadEngine = _lastEngine;
456            } else if (getConsist(getTrain().getSecondLegNumberEngines(), getTrain().getSecondLegEngineModel(),
457                    getTrain().getSecondLegEngineRoad(), getTrain().getSecondLegStartRouteLocation(),
458                    engineTerminatesSecondLeg)) {
459                _secondLeadEngine = _lastEngine;
460                _thirdLeadEngine = _lastEngine;
461            } else {
462                throw new BuildFailedException(
463                        Bundle.getMessage("buildErrorEngines", getTrain().getSecondLegNumberEngines(),
464                                getTrain().getSecondLegStartRouteLocation(), engineTerminatesSecondLeg));
465            }
466        }
467        // Second engine change in route?
468        if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES ||
469                (getTrain().getThirdLegOptions() & Train.ADD_ENGINES) == Train.ADD_ENGINES) {
470            addLine(THREE, BLANK_LINE);
471            if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
472                addLine(THREE,
473                        Bundle.getMessage("buildTrainEngineChange", getTrain().getThirdLegStartLocationName(),
474                                getTrain().getThirdLegNumberEngines(), getTrain().getThirdLegEngineModel(),
475                                getTrain().getThirdLegEngineRoad()));
476            } else {
477                addLine(THREE,
478                        Bundle.getMessage("buildTrainAddEngines", getTrain().getThirdLegNumberEngines(),
479                                getTrain().getThirdLegStartLocationName(), getTrain().getThirdLegEngineModel(),
480                                getTrain().getThirdLegEngineRoad()));
481            }
482            if (getEngines(getTrain().getThirdLegNumberEngines(), getTrain().getThirdLegEngineModel(),
483                    getTrain().getThirdLegEngineRoad(), getTrain().getThirdLegStartRouteLocation(),
484                    getTrain().getTrainTerminatesRouteLocation())) {
485                _thirdLeadEngine = _lastEngine;
486            } else if (getConsist(getTrain().getThirdLegNumberEngines(), getTrain().getThirdLegEngineModel(),
487                    getTrain().getThirdLegEngineRoad(), getTrain().getThirdLegStartRouteLocation(),
488                    getTrain().getTrainTerminatesRouteLocation())) {
489                _thirdLeadEngine = _lastEngine;
490            } else {
491                throw new BuildFailedException(
492                        Bundle.getMessage("buildErrorEngines", Integer.parseInt(getTrain().getThirdLegNumberEngines()),
493                                getTrain().getThirdLegStartRouteLocation(),
494                                getTrain().getTrainTerminatesRouteLocation()));
495            }
496        }
497        if (!getTrain().getNumberEngines().equals("0") &&
498                (!getTrain().isBuildConsistEnabled() || Setup.getHorsePowerPerTon() == 0)) {
499            addLine(SEVEN, Bundle.getMessage("buildDoneAssingEnginesTrain", getTrain().getName()));
500        }
501    }
502
503    protected boolean getConsist(String reqNumEngines, String model, String road, RouteLocation rl, RouteLocation rld)
504            throws BuildFailedException {
505        if (reqNumEngines.equals(Train.AUTO_HPT)) {
506            for (int i = 2; i < Setup.getMaxNumberEngines(); i++) {
507                if (getEngines(Integer.toString(i), model, road, rl, rld)) {
508                    return true;
509                }
510            }
511        }
512        return false;
513    }
514
515    protected void showEnginesByLocation() {
516        // show how many engines were found
517        addLine(SEVEN, BLANK_LINE);
518        addLine(ONE,
519                Bundle.getMessage("buildFoundLocos", Integer.toString(getEngineList().size()), getTrain().getName()));
520
521        // only show engines once using the train's route
522        List<String> locationNames = new ArrayList<>();
523        for (RouteLocation rl : getTrain().getRoute().getLocationsBySequenceList()) {
524            if (locationNames.contains(rl.getName())) {
525                continue;
526            }
527            locationNames.add(rl.getName());
528            int count = countRollingStockAt(rl, new ArrayList<RollingStock>(getEngineList()));
529            if (rl.getLocation().isStaging()) {
530                addLine(FIVE,
531                        Bundle.getMessage("buildLocosInStaging", count, rl.getName()));
532            } else {
533                addLine(FIVE,
534                        Bundle.getMessage("buildLocosAtLocation", count, rl.getName()));
535            }
536            for (Engine engine : getEngineList()) {
537                if (engine.getLocationName().equals(rl.getName())) {
538                    addLine(SEVEN,
539                            Bundle.getMessage("buildLocoAtLocWithMoves", engine.toString(), engine.getTypeName(),
540                                    engine.getModel(), engine.getLocationName(), engine.getTrackName(),
541                                    engine.getMoves()));
542                }
543            }
544            addLine(SEVEN, BLANK_LINE);
545        }
546    }
547
548    /**
549     * Adds engines to the train if needed based on HPT. Note that the engine
550     * additional weight isn't considered in this method so HP requirements can
551     * be lower compared to the original calculation which did include the
552     * weight of the engines.
553     *
554     * @param hpAvailable   the engine hp already assigned to the train for this
555     *                      leg
556     * @param extraHpNeeded the additional hp needed
557     * @param rlNeedHp      where in the route the additional hp is needed
558     * @param rl            the start of the leg
559     * @param rld           the end of the leg
560     * @throws BuildFailedException if unable to add engines to train
561     */
562    protected void addEnginesBasedHPT(int hpAvailable, int extraHpNeeded, RouteLocation rlNeedHp, RouteLocation rl,
563            RouteLocation rld) throws BuildFailedException {
564        if (rlNeedHp == null) {
565            return;
566        }
567        int numberLocos = 0;
568        // determine how many locos have already been assigned to the train
569        List<Engine> engines = engineManager.getList(getTrain());
570        for (Engine rs : engines) {
571            if (rs.getRouteLocation() == rl) {
572                numberLocos++;
573            }
574        }
575
576        addLine(ONE, BLANK_LINE);
577        addLine(ONE, Bundle.getMessage("buildTrainReqExtraHp", extraHpNeeded, rlNeedHp.getName(),
578                rld.getName(), numberLocos));
579
580        // determine engine model and road
581        String model = getTrain().getEngineModel();
582        String road = getTrain().getEngineRoad();
583        if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
584                rl == getTrain().getSecondLegStartRouteLocation()) {
585            model = getTrain().getSecondLegEngineModel();
586            road = getTrain().getSecondLegEngineRoad();
587        } else if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
588                rl == getTrain().getThirdLegStartRouteLocation()) {
589            model = getTrain().getThirdLegEngineModel();
590            road = getTrain().getThirdLegEngineRoad();
591        }
592
593        while (numberLocos < Setup.getMaxNumberEngines()) {
594            // if no engines assigned, can't use B unit as first engine
595            if (!getEngines("1", model, road, rl, rld, numberLocos > 0)) {
596                throw new BuildFailedException(Bundle.getMessage("buildErrorEngines", Bundle.getMessage("additional"),
597                        rl.getName(), rld.getName()));
598            }
599            numberLocos++;
600            int currentHp = getTrain().getTrainHorsePower(rlNeedHp);
601            if (currentHp > hpAvailable + extraHpNeeded) {
602                break; // done
603            }
604            if (numberLocos < Setup.getMaxNumberEngines()) {
605                addLine(FIVE, BLANK_LINE);
606                addLine(THREE,
607                        Bundle.getMessage("buildContinueAddLocos", (hpAvailable + extraHpNeeded - currentHp),
608                                rlNeedHp.getName(), rld.getName(), numberLocos, currentHp));
609            } else {
610                addLine(FIVE,
611                        Bundle.getMessage("buildMaxNumberLocoAssigned", Setup.getMaxNumberEngines()));
612            }
613        }
614    }
615
616    /**
617     * Checks to see if the engine or consist assigned to the train has the
618     * appropriate HP. If the train's HP requirements are significantly higher
619     * or lower than the engine that was assigned, the program will search for a
620     * more appropriate engine or consist, and assign that engine or consist to
621     * the train. The HP calculation is based on a minimum train speed of 36
622     * MPH. The formula HPT x 12 / % Grade = Speed, is used to determine the
623     * horsepower required. Speed is fixed at 36 MPH. For example a 1% grade
624     * requires a minimum of 3 HPT. Disabled for trains departing staging.
625     * 
626     * @throws BuildFailedException if coding error.
627     */
628    protected void checkEngineHP() throws BuildFailedException {
629        if (Setup.getHorsePowerPerTon() != 0) {
630            if (getTrain().getNumberEngines().equals(Train.AUTO_HPT)) {
631                checkEngineHP(getTrain().getLeadEngine(), getTrain().getEngineModel(), getTrain().getEngineRoad()); // 1st
632                // leg
633            }
634            if ((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
635                    getTrain().getSecondLegNumberEngines().equals(Train.AUTO_HPT)) {
636                checkEngineHP(_secondLeadEngine, getTrain().getSecondLegEngineModel(),
637                        getTrain().getSecondLegEngineRoad());
638            }
639            if ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
640                    getTrain().getThirdLegNumberEngines().equals(Train.AUTO_HPT)) {
641                checkEngineHP(_thirdLeadEngine, getTrain().getThirdLegEngineModel(),
642                        getTrain().getThirdLegEngineRoad());
643            }
644        }
645    }
646
647    private void checkEngineHP(Engine leadEngine, String model, String road) throws BuildFailedException {
648        // code check
649        if (leadEngine == null) {
650            throw new BuildFailedException("ERROR coding issue, engine missing from checkEngineHP()");
651        }
652        // departing staging?
653        if (leadEngine.getRouteLocation() == getTrain().getTrainDepartsRouteLocation() &&
654                getTrain().isDepartingStaging()) {
655            return;
656        }
657        addLine(ONE, BLANK_LINE);
658        addLine(ONE,
659                Bundle.getMessage("buildDetermineHpNeeded", leadEngine.toString(), leadEngine.getLocationName(),
660                        leadEngine.getDestinationName(), getTrain().getTrainHorsePower(leadEngine.getRouteLocation()),
661                        Setup.getHorsePowerPerTon()));
662        // now determine the HP needed for this train
663        double hpNeeded = 0;
664        int hpAvailable = 0;
665        Route route = getTrain().getRoute();
666        if (route != null) {
667            boolean helper = false;
668            boolean foundStart = false;
669            for (RouteLocation rl : route.getLocationsBySequenceList()) {
670                if (!foundStart && rl != leadEngine.getRouteLocation()) {
671                    continue;
672                }
673                foundStart = true;
674                if ((getTrain().getSecondLegOptions() == Train.HELPER_ENGINES &&
675                        rl == getTrain().getSecondLegStartRouteLocation()) ||
676                        (getTrain().getThirdLegOptions() == Train.HELPER_ENGINES &&
677                                rl == getTrain().getThirdLegStartRouteLocation())) {
678                    addLine(FIVE,
679                            Bundle.getMessage("AddHelpersAt", rl.getName()));
680                    helper = true;
681                }
682                if ((getTrain().getSecondLegOptions() == Train.HELPER_ENGINES &&
683                        rl == getTrain().getSecondLegEndRouteLocation()) ||
684                        (getTrain().getThirdLegOptions() == Train.HELPER_ENGINES &&
685                                rl == getTrain().getThirdLegEndRouteLocation())) {
686                    addLine(FIVE,
687                            Bundle.getMessage("RemoveHelpersAt", rl.getName()));
688                    helper = false;
689                }
690                if (helper) {
691                    continue; // ignore HP needed when helpers are assigned to
692                              // the train
693                }
694                // check for a change of engines in the train's route
695                if (rl == leadEngine.getRouteDestination()) {
696                    log.debug("Remove loco ({}) at ({})", leadEngine.toString(), rl.getName());
697                    break; // done
698                }
699                if (getTrain().getTrainHorsePower(rl) > hpAvailable)
700                    hpAvailable = getTrain().getTrainHorsePower(rl);
701                int weight = rl.getTrainWeight();
702                double hpRequired = (Control.speedHpt * rl.getGrade() / 12) * weight;
703                if (hpRequired < Setup.getHorsePowerPerTon() * weight)
704                    hpRequired = Setup.getHorsePowerPerTon() * weight; // min HPT
705                if (hpRequired > hpNeeded) {
706                    addLine(SEVEN,
707                            Bundle.getMessage("buildReportTrainHpNeeds", weight, getTrain().getNumberCarsInTrain(rl),
708                                    rl.getGrade(), rl.getName(), rl.getId(), hpRequired));
709                    hpNeeded = hpRequired;
710                }
711            }
712        }
713        if (hpNeeded > hpAvailable) {
714            addLine(ONE,
715                    Bundle.getMessage("buildAssignedHpNotEnough", leadEngine.toString(), hpAvailable, hpNeeded));
716            getNewEngine((int) hpNeeded, leadEngine, model, road);
717        } else if (hpAvailable > 2 * hpNeeded) {
718            addLine(ONE,
719                    Bundle.getMessage("buildAssignedHpTooMuch", leadEngine.toString(), hpAvailable, hpNeeded));
720            getNewEngine((int) hpNeeded, leadEngine, model, road);
721        } else {
722            log.debug("Keeping engine ({}) it meets the train's HP requirement", leadEngine.toString());
723        }
724    }
725
726    /**
727     * Removes engine from train and attempts to replace it with engine or
728     * consist that meets the HP requirements of the train.
729     *
730     * @param hpNeeded   How much hp is needed
731     * @param leadEngine The lead engine for this leg
732     * @param model      The engine's model
733     * @param road       The engine's road
734     * @throws BuildFailedException if new engine not found
735     */
736    protected void getNewEngine(int hpNeeded, Engine leadEngine, String model, String road)
737            throws BuildFailedException {
738        // save lead engine's rl, and rld
739        RouteLocation rl = leadEngine.getRouteLocation();
740        RouteLocation rld = leadEngine.getRouteDestination();
741        removeEngineFromTrain(leadEngine);
742        getEngineList().add(0, leadEngine); // put engine back into the pool
743        if (hpNeeded < 50) {
744            hpNeeded = 50; // the minimum HP
745        }
746        int hpMax = hpNeeded;
747        // largest single engine HP known today is less than 15,000.
748        // high end modern diesel locos approximately 5000 HP.
749        // 100 car train at 100 tons per car and 2 HPT requires 20,000 HP.
750        // will assign consisted engines to train.
751        boolean foundLoco = false;
752        List<Engine> rejectedLocos = new ArrayList<>();
753        hpLoop: while (hpMax < 20000) {
754            hpMax += hpNeeded / 2; // start off looking for an engine with no
755                                   // more than 50% extra HP
756            log.debug("Max hp {}", hpMax);
757            for (Engine engine : getEngineList()) {
758                if (rejectedLocos.contains(engine)) {
759                    continue;
760                }
761                // don't use non lead locos in a consist
762                if (engine.getConsist() != null && !engine.isLead()) {
763                    continue;
764                }
765                if (engine.getLocation() != rl.getLocation()) {
766                    continue;
767                }
768                if (!model.equals(Train.NONE) && !engine.getModel().equals(model)) {
769                    continue;
770                }
771                if (!road.equals(Train.NONE) && !engine.getRoadName().equals(road) ||
772                        road.equals(Train.NONE) && !getTrain().isLocoRoadNameAccepted(engine.getRoadName())) {
773                    continue;
774                }
775                int engineHp = engine.getHpInteger();
776                if (engine.getConsist() != null) {
777                    for (Engine e : engine.getConsist().getEngines()) {
778                        if (e != engine) {
779                            engineHp = engineHp + e.getHpInteger();
780                        }
781                    }
782                }
783                if (engineHp > hpNeeded && engineHp <= hpMax) {
784                    addLine(FIVE,
785                            Bundle.getMessage("buildLocoHasRequiredHp", engine.toString(), engineHp, hpNeeded));
786                    if (setEngineDestination(engine, rl, rld)) {
787                        foundLoco = true;
788                        break hpLoop;
789                    } else {
790                        rejectedLocos.add(engine);
791                    }
792                }
793            }
794        }
795        if (!foundLoco && !getTrain().isBuildConsistEnabled()) {
796            throw new BuildFailedException(Bundle.getMessage("buildErrorEngHp", rl.getLocation().getName()));
797        }
798    }
799
800    /**
801     * Sets the destination track for an engine and assigns it to the train.
802     *
803     * @param engine The engine to be added to train
804     * @param rl     Departure route location
805     * @param rld    Destination route location
806     * @return true if destination track found and set
807     */
808    protected boolean setEngineDestination(Engine engine, RouteLocation rl, RouteLocation rld) {
809        // engine to staging?
810        if (rld == getTrain().getTrainTerminatesRouteLocation() && getTerminateStagingTrack() != null) {
811            String status =
812                    engine.checkDestination(getTerminateStagingTrack().getLocation(), getTerminateStagingTrack());
813            if (status.equals(Track.OKAY)) {
814                addEngineToTrain(engine, rl, rld, getTerminateStagingTrack());
815                return true; // done
816            } else {
817                addLine(SEVEN,
818                        Bundle.getMessage("buildCanNotDropEngineToTrack", engine.toString(),
819                                getTerminateStagingTrack().getTrackTypeName(),
820                                getTerminateStagingTrack().getLocation().getName(),
821                                getTerminateStagingTrack().getName(), status));
822            }
823        } else {
824            // find a destination track for this engine
825            Location destination = rld.getLocation();
826            List<Track> destTracks = destination.getTracksByMoves(null);
827            if (destTracks.size() == 0) {
828                addLine(THREE, Bundle.getMessage("buildNoTracksAtDestination", rld.getName()));
829            }
830            for (Track track : destTracks) {
831                if (!checkDropTrainDirection(engine, rld, track)) {
832                    continue;
833                }
834                if (!checkTrainCanDrop(engine, track)) {
835                    continue;
836                }
837                String status = engine.checkDestination(destination, track);
838                if (status.equals(Track.OKAY)) {
839                    addLine(FIVE,
840                            Bundle.getMessage("buildEngineCanDrop", engine.toString(),
841                                    track.getTrackTypeName(),
842                                    track.getLocation().getName(), track.getName()));
843                    addEngineToTrain(engine, rl, rld, track);
844                    return true;
845                } else {
846                    addLine(SEVEN,
847                            Bundle.getMessage("buildCanNotDropEngineToTrack", engine.toString(),
848                                    track.getTrackTypeName(),
849                                    track.getLocation().getName(), track.getName(), status));
850                }
851            }
852            addLine(FIVE,
853                    Bundle.getMessage("buildCanNotDropEngToDest", engine.toString(), rld.getName()));
854        }
855        return false; // not able to set loco's destination
856    }
857
858    /**
859     * Adds an engine to the train.
860     *
861     * @param engine the engine being added to the train
862     * @param rl     where in the train's route to pick up the engine
863     * @param rld    where in the train's route to set out the engine
864     * @param track  the destination track for this engine
865     */
866    private void addEngineToTrain(Engine engine, RouteLocation rl, RouteLocation rld, Track track) {
867        _lastEngine = engine; // needed in case there's a engine change in the
868                              // train's route
869        engine = checkQuickServiceArrival(engine, rld, track);
870        if (getTrain().getLeadEngine() == null) {
871            getTrain().setLeadEngine(engine); // load lead engine
872        }
873        addLine(ONE, Bundle.getMessage("buildEngineAssigned", engine.toString(), rl.getName(),
874                rld.getName(), track.getName()));
875        engine.setDestination(track.getLocation(), track, Engine.FORCE);
876        int length = engine.getTotalLength();
877        int weightTons = engine.getAdjustedWeightTons();
878        // engine in consist?
879        if (engine.getConsist() != null) {
880            length = engine.getConsist().getTotalLength();
881            weightTons = engine.getConsist().getAdjustedWeightTons();
882            for (Engine cEngine : engine.getConsist().getEngines()) {
883                if (cEngine != engine) {
884                    addLine(ONE, Bundle.getMessage("buildEngineAssigned", cEngine.toString(),
885                            rl.getName(), rld.getName(), track.getName()));
886                    cEngine.setTrain(getTrain());
887                    cEngine.setRouteLocation(rl);
888                    cEngine.setRouteDestination(rld);
889                    cEngine.setDestination(track.getLocation(), track, RollingStock.FORCE); // force
890                }
891            }
892        }
893        // now adjust train length and weight for each location that engines are
894        // in the train
895        finishAddRsToTrain(engine, rl, rld, length, weightTons);
896    }
897
898    /**
899     * Checks to see if track is requesting a quick service. Since it isn't
900     * possible for a engine to be pulled and set out twice, this code creates a
901     * "clone" engine to create the requested Manifest. A engine could have
902     * multiple clones, therefore each clone has a creation order number. The
903     * first clone is used to restore a engine's location in the case of reset.
904     * 
905     * @param engine the engine possibly needing quick service
906     * @param track  the destination track
907     * @return the engine if not quick service, or a clone if quick service
908     */
909    private Engine checkQuickServiceArrival(Engine engine, RouteLocation rld, Track track) {
910        if (!track.isQuickServiceEnabled()) {
911            return engine;
912        }
913        addLine(FIVE,
914                Bundle.getMessage("buildTrackQuickService", StringUtils.capitalize(track.getTrackTypeName()),
915                        track.getLocation().getName(), track.getName()));
916        // quick service enabled, create clones
917        Engine cloneEng = engineManager.createClone(engine, track, getTrain(), getStartTime());
918        // for timing, use arrival times for the train that is building
919        // other trains will use their departure time, loaded when creating the Manifest
920        String expectedArrivalTime = getTrain().getExpectedArrivalTime(rld, true);
921        cloneEng.setSetoutTime(expectedArrivalTime);
922        // remember where in the route the car was delivered
923        engine.setRouteDestination(rld);
924        return cloneEng; // return clone
925    }
926
927    /**
928     * Checks to see if additional engines are needed for the train based on the
929     * train's calculated tonnage. Minimum speed for the train is fixed at 36
930     * MPH. The formula HPT x 12 / % Grade = Speed, is used to determine the
931     * horsepower needed. For example a 1% grade requires a minimum of 3 HPT.
932     * Ignored when departing staging
933     *
934     * @throws BuildFailedException if build failure
935     */
936    protected void checkNumnberOfEnginesNeededHPT() throws BuildFailedException {
937        if (!getTrain().isBuildConsistEnabled() ||
938                Setup.getHorsePowerPerTon() == 0) {
939            return;
940        }
941        addLine(ONE, BLANK_LINE);
942        addLine(ONE, Bundle.getMessage("buildDetermineNeeds", Setup.getHorsePowerPerTon()));
943        Route route = getTrain().getRoute();
944        int hpAvailable = 0;
945        int extraHpNeeded = 0;
946        RouteLocation rlNeedHp = null;
947        RouteLocation rlStart = getTrain().getTrainDepartsRouteLocation();
948        RouteLocation rlEnd = getTrain().getTrainTerminatesRouteLocation();
949        boolean departingStaging = getTrain().isDepartingStaging();
950        if (route != null) {
951            boolean helper = false;
952            for (RouteLocation rl : route.getLocationsBySequenceList()) {
953                if ((getTrain().getSecondLegOptions() == Train.HELPER_ENGINES &&
954                        rl == getTrain().getSecondLegStartRouteLocation()) ||
955                        (getTrain().getThirdLegOptions() == Train.HELPER_ENGINES &&
956                                rl == getTrain().getThirdLegStartRouteLocation())) {
957                    addLine(FIVE, Bundle.getMessage("AddHelpersAt", rl.getName()));
958                    helper = true;
959                }
960                if ((getTrain().getSecondLegOptions() == Train.HELPER_ENGINES &&
961                        rl == getTrain().getSecondLegEndRouteLocation()) ||
962                        (getTrain().getThirdLegOptions() == Train.HELPER_ENGINES &&
963                                rl == getTrain().getThirdLegEndRouteLocation())) {
964                    addLine(FIVE,
965                            Bundle.getMessage("RemoveHelpersAt", rl.getName()));
966                    helper = false;
967                }
968                if (helper) {
969                    continue;
970                }
971                // check for a change of engines in the train's route
972                if (((getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
973                        rl == getTrain().getSecondLegStartRouteLocation()) ||
974                        ((getTrain().getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
975                                rl == getTrain().getThirdLegStartRouteLocation())) {
976                    log.debug("Loco change at ({})", rl.getName());
977                    addEnginesBasedHPT(hpAvailable, extraHpNeeded, rlNeedHp, rlStart, rl);
978                    addLine(THREE, BLANK_LINE);
979                    // reset for next leg of train's route
980                    rlStart = rl;
981                    rlNeedHp = null;
982                    extraHpNeeded = 0;
983                    departingStaging = false;
984                }
985                if (departingStaging) {
986                    continue;
987                }
988                double weight = rl.getTrainWeight();
989                if (weight > 0) {
990                    double hptMinimum = Setup.getHorsePowerPerTon();
991                    double hptGrade = (Control.speedHpt * rl.getGrade() / 12);
992                    double hp = getTrain().getTrainHorsePower(rl);
993                    double hpt = hp / weight;
994                    if (hptGrade > hptMinimum) {
995                        hptMinimum = hptGrade;
996                    }
997                    if (hptMinimum > hpt) {
998                        int addHp = (int) (hptMinimum * weight - hp);
999                        if (addHp > extraHpNeeded) {
1000                            hpAvailable = (int) hp;
1001                            extraHpNeeded = addHp;
1002                            rlNeedHp = rl;
1003                        }
1004                        addLine(SEVEN,
1005                                Bundle.getMessage("buildAddLocosStatus", weight, hp, Control.speedHpt, rl.getGrade(),
1006                                        hpt, hptMinimum, rl.getName(), rl.getId()));
1007                        addLine(FIVE,
1008                                Bundle.getMessage("buildTrainRequiresAddHp", addHp, rl.getName(), hptMinimum));
1009                    }
1010                }
1011            }
1012        }
1013        addEnginesBasedHPT(hpAvailable, extraHpNeeded, rlNeedHp, rlStart, rlEnd);
1014        addLine(SEVEN, Bundle.getMessage("buildDoneAssingEnginesTrain", getTrain().getName()));
1015        addLine(THREE, BLANK_LINE);
1016    }
1017
1018    protected void removeEngineFromTrain(Engine engine) {
1019        // replace lead engine?
1020        if (getTrain().getLeadEngine() == engine) {
1021            getTrain().setLeadEngine(null);
1022        }
1023        if (engine.getConsist() != null) {
1024            for (Engine e : engine.getConsist().getEngines()) {
1025                removeRollingStockFromTrain(e);
1026            }
1027        } else {
1028            removeRollingStockFromTrain(engine);
1029        }
1030    }
1031
1032    private final static Logger log = LoggerFactory.getLogger(TrainBuilderEngines.class);
1033}