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}