001package jmri.jmrit.operations.trains;
002
003import org.slf4j.Logger;
004import org.slf4j.LoggerFactory;
005
006import jmri.jmrit.operations.rollingstock.engines.Engine;
007import jmri.jmrit.operations.routes.Route;
008import jmri.jmrit.operations.routes.RouteLocation;
009import jmri.jmrit.operations.setup.Setup;
010
011/**
012 * Contains methods for engines when building a train.
013 * 
014 * @author Daniel Boudreau Copyright (C) 2022
015 */
016public class TrainBuilderEngines extends TrainBuilderBase {
017
018    /**
019     * Builds a list of possible engines for this train.
020     */
021    protected void getAndRemoveEnginesFromList() {
022        _engineList = engineManager.getAvailableTrainList(_train);
023
024        // remove any locos that the train can't use
025        for (int indexEng = 0; indexEng < _engineList.size(); indexEng++) {
026            Engine engine = _engineList.get(indexEng);
027            // remove engines types that train does not service
028            if (!_train.isTypeNameAccepted(engine.getTypeName())) {
029                addLine(_buildReport, SEVEN, Bundle.getMessage("buildExcludeEngineType", engine.toString(),
030                        engine.getLocationName(), engine.getTrackName(), engine.getTypeName()));
031                _engineList.remove(indexEng--);
032                continue;
033            }
034            // remove engines with roads that train does not service
035            if (!_train.isLocoRoadNameAccepted(engine.getRoadName())) {
036                addLine(_buildReport, SEVEN, Bundle.getMessage("buildExcludeEngineRoad", engine.toString(),
037                        engine.getLocationName(), engine.getTrackName(), engine.getRoadName()));
038                _engineList.remove(indexEng--);
039                continue;
040            }
041            // remove engines with owners that train does not service
042            if (!_train.isOwnerNameAccepted(engine.getOwnerName())) {
043                addLine(_buildReport, SEVEN, Bundle.getMessage("buildExcludeEngineOwner", engine.toString(),
044                        engine.getLocationName(), engine.getTrackName(), engine.getOwnerName()));
045                _engineList.remove(indexEng--);
046                continue;
047            }
048            // remove engines with built dates that train does not service
049            if (!_train.isBuiltDateAccepted(engine.getBuilt())) {
050                addLine(_buildReport, SEVEN, Bundle.getMessage("buildExcludeEngineBuilt", engine.toString(),
051                        engine.getLocationName(), engine.getTrackName(), engine.getBuilt()));
052                _engineList.remove(indexEng--);
053                continue;
054            }
055            // remove engines that are out of service
056            if (engine.isOutOfService()) {
057                addLine(_buildReport, SEVEN, Bundle.getMessage("buildExcludeEngineOutOfService", engine.toString(),
058                        engine.getLocationName(), engine.getTrackName()));
059                _engineList.remove(indexEng--);
060                continue;
061            }
062            // remove engines that aren't on the train's route
063            if (_train.getRoute().getLastLocationByName(engine.getLocationName()) == null) {
064                log.debug("removing engine ({}) location ({}) not serviced by train", engine.toString(),
065                        engine.getLocationName());
066                _engineList.remove(indexEng--);
067                continue;
068            }
069        }
070    }
071
072    /**
073     * Adds engines to the train starting at the first location in the train's
074     * route. Note that engines from staging are already part of the train.
075     * There can be up to two engine swaps in a train's route.
076     * 
077     * @throws BuildFailedException if required engines can't be added to train.
078     */
079    protected void addEnginesToTrain() throws BuildFailedException {
080        // allow up to two engine and caboose swaps in the train's route
081        RouteLocation engineTerminatesFirstLeg = _train.getTrainTerminatesRouteLocation();
082        RouteLocation engineTerminatesSecondLeg = _train.getTrainTerminatesRouteLocation();
083
084        // Adjust where the locos will terminate
085        if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
086                _train.getSecondLegStartRouteLocation() != null) {
087            engineTerminatesFirstLeg = _train.getSecondLegStartRouteLocation();
088        }
089        if ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
090                _train.getThirdLegStartRouteLocation() != null) {
091            engineTerminatesSecondLeg = _train.getThirdLegStartRouteLocation();
092            // No engine or caboose change at first leg?
093            if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) != Train.CHANGE_ENGINES) {
094                engineTerminatesFirstLeg = _train.getThirdLegStartRouteLocation();
095            }
096        }
097
098        // load engines at the start of the route for this train
099        if (_train.getLeadEngine() == null) {
100            addLine(_buildReport, THREE, BLANK_LINE);
101            if (getEngines(_train.getNumberEngines(), _train.getEngineModel(), _train.getEngineRoad(),
102                    _train.getTrainDepartsRouteLocation(), engineTerminatesFirstLeg)) {
103                _secondLeadEngine = _lastEngine; // when adding a caboose later
104                                                 // in the route, no engine
105                                                 // change
106                _thirdLeadEngine = _lastEngine;
107            } else if (getConsist(_train.getNumberEngines(), _train.getEngineModel(), _train.getEngineRoad(),
108                    _train.getTrainDepartsRouteLocation(), engineTerminatesFirstLeg)) {
109                _secondLeadEngine = _lastEngine; // when adding a caboose later
110                                                 // in the route, no engine
111                                                 // change
112                _thirdLeadEngine = _lastEngine;
113            } else {
114                throw new BuildFailedException(Bundle.getMessage("buildErrorEngines", _train.getNumberEngines(),
115                        _train.getTrainDepartsName(), engineTerminatesFirstLeg.getName()));
116            }
117        }
118
119        // First engine change in route?
120        if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
121            addLine(_buildReport, THREE, BLANK_LINE);
122            addLine(_buildReport, THREE,
123                    Bundle.getMessage("buildTrainEngineChange", _train.getSecondLegStartLocationName(),
124                            _train.getSecondLegNumberEngines(), _train.getSecondLegEngineModel(),
125                            _train.getSecondLegEngineRoad()));
126            if (getEngines(_train.getSecondLegNumberEngines(), _train.getSecondLegEngineModel(),
127                    _train.getSecondLegEngineRoad(), _train.getSecondLegStartRouteLocation(),
128                    engineTerminatesSecondLeg)) {
129                _secondLeadEngine = _lastEngine;
130                _thirdLeadEngine = _lastEngine;
131            } else if (getConsist(_train.getSecondLegNumberEngines(), _train.getSecondLegEngineModel(),
132                    _train.getSecondLegEngineRoad(), _train.getSecondLegStartRouteLocation(),
133                    engineTerminatesSecondLeg)) {
134                _secondLeadEngine = _lastEngine;
135                _thirdLeadEngine = _lastEngine;
136            } else {
137                throw new BuildFailedException(
138                        Bundle.getMessage("buildErrorEngines", _train.getSecondLegNumberEngines(),
139                                _train.getSecondLegStartRouteLocation(), engineTerminatesSecondLeg));
140            }
141        }
142        // Second engine change in route?
143        if ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
144            addLine(_buildReport, THREE, BLANK_LINE);
145            addLine(_buildReport, THREE,
146                    Bundle.getMessage("buildTrainEngineChange", _train.getThirdLegStartLocationName(),
147                            _train.getThirdLegNumberEngines(), _train.getThirdLegEngineModel(),
148                            _train.getThirdLegEngineRoad()));
149            if (getEngines(_train.getThirdLegNumberEngines(), _train.getThirdLegEngineModel(),
150                    _train.getThirdLegEngineRoad(), _train.getThirdLegStartRouteLocation(),
151                    _train.getTrainTerminatesRouteLocation())) {
152                _thirdLeadEngine = _lastEngine;
153            } else if (getConsist(_train.getThirdLegNumberEngines(), _train.getThirdLegEngineModel(),
154                    _train.getThirdLegEngineRoad(), _train.getThirdLegStartRouteLocation(),
155                    _train.getTrainTerminatesRouteLocation())) {
156                _thirdLeadEngine = _lastEngine;
157            } else {
158                throw new BuildFailedException(
159                        Bundle.getMessage("buildErrorEngines", Integer.parseInt(_train.getThirdLegNumberEngines()),
160                                _train.getThirdLegStartRouteLocation(), _train.getTrainTerminatesRouteLocation()));
161            }
162        }
163        if (!_train.getNumberEngines().equals("0") &&
164                (!_train.isBuildConsistEnabled() || Setup.getHorsePowerPerTon() == 0)) {
165            addLine(_buildReport, SEVEN, Bundle.getMessage("buildDoneAssingEnginesTrain", _train.getName()));
166        }
167    }
168
169    /**
170     * Checks to see if the engine or consist assigned to the train has the
171     * appropriate HP. If the train's HP requirements are significantly higher
172     * or lower than the engine that was assigned, the program will search for a
173     * more appropriate engine or consist, and assign that engine or consist to
174     * the train. The HP calculation is based on a minimum train speed of 36
175     * MPH. The formula HPT x 12 / % Grade = Speed, is used to determine the
176     * horsepower required. Speed is fixed at 36 MPH. For example a 1% grade
177     * requires a minimum of 3 HPT. Disabled for trains departing staging.
178     * 
179     * @throws BuildFailedException if coding error.
180     */
181    protected void checkEngineHP() throws BuildFailedException {
182        if (Setup.getHorsePowerPerTon() != 0) {
183            if (_train.getNumberEngines().equals(Train.AUTO_HPT)) {
184                checkEngineHP(_train.getLeadEngine(), _train.getEngineModel(), _train.getEngineRoad()); // 1st
185                                                                                                        // leg
186            }
187            if ((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
188                    _train.getSecondLegNumberEngines().equals(Train.AUTO_HPT)) {
189                checkEngineHP(_secondLeadEngine, _train.getSecondLegEngineModel(), _train.getSecondLegEngineRoad());
190            }
191            if ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
192                    _train.getThirdLegNumberEngines().equals(Train.AUTO_HPT)) {
193                checkEngineHP(_thirdLeadEngine, _train.getThirdLegEngineModel(), _train.getThirdLegEngineRoad());
194            }
195        }
196    }
197
198    private void checkEngineHP(Engine leadEngine, String model, String road) throws BuildFailedException {
199        // code check
200        if (leadEngine == null) {
201            throw new BuildFailedException("ERROR coding issue, engine missing from checkEngineHP()");
202        }
203        // departing staging?
204        if (leadEngine.getRouteLocation() == _train.getTrainDepartsRouteLocation() && _departStageTrack != null) {
205            return;
206        }
207        addLine(_buildReport, ONE, BLANK_LINE);
208        addLine(_buildReport, ONE,
209                Bundle.getMessage("buildDetermineHpNeeded", leadEngine.toString(), leadEngine.getLocationName(),
210                        leadEngine.getDestinationName(), _train.getTrainHorsePower(leadEngine.getRouteLocation()),
211                        Setup.getHorsePowerPerTon()));
212        // now determine the HP needed for this train
213        int hpNeeded = 0;
214        int hpAvailable = 0;
215        Route route = _train.getRoute();
216        if (route != null) {
217            boolean helper = false;
218            boolean foundStart = false;
219            for (RouteLocation rl : route.getLocationsBySequenceList()) {
220                if (!foundStart && rl != leadEngine.getRouteLocation()) {
221                    continue;
222                }
223                foundStart = true;
224                if ((_train.getSecondLegOptions() == Train.HELPER_ENGINES &&
225                        rl == _train.getSecondLegStartRouteLocation()) ||
226                        (_train.getThirdLegOptions() == Train.HELPER_ENGINES &&
227                                rl == _train.getThirdLegStartRouteLocation())) {
228                    addLine(_buildReport, FIVE,
229                            Bundle.getMessage("AddHelpersAt", rl.getName()));
230                    helper = true;
231                }
232                if ((_train.getSecondLegOptions() == Train.HELPER_ENGINES &&
233                        rl == _train.getSecondLegEndRouteLocation()) ||
234                        (_train.getThirdLegOptions() == Train.HELPER_ENGINES &&
235                                rl == _train.getThirdLegEndRouteLocation())) {
236                    addLine(_buildReport, FIVE,
237                            Bundle.getMessage("RemoveHelpersAt", rl.getName()));
238                    helper = false;
239                }
240                if (helper) {
241                    continue; // ignore HP needed when helpers are assigned to
242                              // the train
243                }
244                // check for a change of engines in the train's route
245                if (rl == leadEngine.getRouteDestination()) {
246                    log.debug("Remove loco ({}) at ({})", leadEngine.toString(), rl.getName());
247                    break; // done
248                }
249                if (_train.getTrainHorsePower(rl) > hpAvailable)
250                    hpAvailable = _train.getTrainHorsePower(rl);
251                int weight = rl.getTrainWeight();
252                int hpRequired = (int) ((36 * rl.getGrade() / 12) * weight);
253                if (hpRequired < Setup.getHorsePowerPerTon() * weight)
254                    hpRequired = Setup.getHorsePowerPerTon() * weight; // minimum
255                                                                       // HPT
256                if (hpRequired > hpNeeded) {
257                    addLine(_buildReport, SEVEN,
258                            Bundle.getMessage("buildReportTrainHpNeeds", weight, _train.getNumberCarsInTrain(rl),
259                                    rl.getGrade(), rl.getName(), rl.getId(), hpRequired));
260                    hpNeeded = hpRequired;
261                }
262            }
263        }
264        if (hpNeeded > hpAvailable) {
265            addLine(_buildReport, ONE,
266                    Bundle.getMessage("buildAssignedHpNotEnough", leadEngine.toString(), hpAvailable, hpNeeded));
267            getNewEngine(hpNeeded, leadEngine, model, road);
268        } else if (hpAvailable > 2 * hpNeeded) {
269            addLine(_buildReport, ONE,
270                    Bundle.getMessage("buildAssignedHpTooMuch", leadEngine.toString(), hpAvailable, hpNeeded));
271            getNewEngine(hpNeeded, leadEngine, model, road);
272        } else {
273            log.debug("Keeping engine ({}) it meets the train's HP requirement", leadEngine.toString());
274        }
275    }
276
277    /**
278     * Checks to see if additional engines are needed for the train based on the
279     * train's calculated tonnage. Minimum speed for the train is fixed at 36
280     * MPH. The formula HPT x 12 / % Grade = Speed, is used to determine the
281     * horsepower needed. For example a 1% grade requires a minimum of 3 HPT.
282     *
283     * @throws BuildFailedException if build failure
284     */
285    protected void checkNumnberOfEnginesNeededHPT() throws BuildFailedException {
286        if (_train.getNumberEngines().equals("0") ||
287                !_train.isBuildConsistEnabled() ||
288                Setup.getHorsePowerPerTon() == 0) {
289            return;
290        }
291        addLine(_buildReport, ONE, BLANK_LINE);
292        addLine(_buildReport, ONE, Bundle.getMessage("buildDetermineNeeds", Setup.getHorsePowerPerTon()));
293        Route route = _train.getRoute();
294        int hpAvailable = 0;
295        int extraHpNeeded = 0;
296        RouteLocation rlNeedHp = null;
297        RouteLocation rlStart = _train.getTrainDepartsRouteLocation();
298        RouteLocation rlEnd = _train.getTrainTerminatesRouteLocation();
299        if (route != null) {
300            boolean helper = false;
301            for (RouteLocation rl : route.getLocationsBySequenceList()) {
302                if ((_train.getSecondLegOptions() == Train.HELPER_ENGINES &&
303                        rl == _train.getSecondLegStartRouteLocation()) ||
304                        (_train.getThirdLegOptions() == Train.HELPER_ENGINES &&
305                                rl == _train.getThirdLegStartRouteLocation())) {
306                    addLine(_buildReport, FIVE, Bundle.getMessage("AddHelpersAt", rl.getName()));
307                    helper = true;
308                }
309                if ((_train.getSecondLegOptions() == Train.HELPER_ENGINES &&
310                        rl == _train.getSecondLegEndRouteLocation()) ||
311                        (_train.getThirdLegOptions() == Train.HELPER_ENGINES &&
312                                rl == _train.getThirdLegEndRouteLocation())) {
313                    addLine(_buildReport, FIVE,
314                            Bundle.getMessage("RemoveHelpersAt", rl.getName()));
315                    helper = false;
316                }
317                if (helper) {
318                    continue;
319                }
320                // check for a change of engines in the train's route
321                if (((_train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
322                        rl == _train.getSecondLegStartRouteLocation()) ||
323                        ((_train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
324                                rl == _train.getThirdLegStartRouteLocation())) {
325                    log.debug("Loco change at ({})", rl.getName());
326                    addEnginesBasedHPT(hpAvailable, extraHpNeeded, rlNeedHp, rlStart, rl);
327                    addLine(_buildReport, THREE, BLANK_LINE);
328                    // reset for next leg of train's route
329                    rlStart = rl;
330                    rlNeedHp = null;
331                    extraHpNeeded = 0;
332                }
333                int weight = rl.getTrainWeight();
334                if (weight > 0) {
335                    double hptMinimum = Setup.getHorsePowerPerTon();
336                    double hptGrade = (36 * rl.getGrade() / 12);
337                    int hp = _train.getTrainHorsePower(rl);
338                    int hpt = hp / weight;
339                    if (hptGrade > hptMinimum) {
340                        hptMinimum = hptGrade;
341                    }
342                    if (hptMinimum > hpt) {
343                        int addHp = (int) (hptMinimum * weight - hp);
344                        if (addHp > extraHpNeeded) {
345                            hpAvailable = hp;
346                            extraHpNeeded = addHp;
347                            rlNeedHp = rl;
348                        }
349                        addLine(_buildReport, SEVEN, Bundle.getMessage("buildAddLocosStatus", weight, hp, rl.getGrade(),
350                                hpt, hptMinimum, rl.getName(), rl.getId()));
351                        addLine(_buildReport, FIVE,
352                                Bundle.getMessage("buildTrainRequiresAddHp", addHp, rl.getName(), hptMinimum));
353                    }
354                }
355            }
356        }
357        addEnginesBasedHPT(hpAvailable, extraHpNeeded, rlNeedHp, rlStart, rlEnd);
358        addLine(_buildReport, SEVEN, Bundle.getMessage("buildDoneAssingEnginesTrain", _train.getName()));
359        addLine(_buildReport, THREE, BLANK_LINE);
360    }
361
362    private final static Logger log = LoggerFactory.getLogger(TrainBuilderEngines.class);
363}