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