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