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}