001package jmri.jmrit.operations.rollingstock.cars; 002 003import java.beans.PropertyChangeEvent; 004import java.text.NumberFormat; 005import java.time.LocalDateTime; 006import java.util.*; 007 008import jmri.*; 009import jmri.jmrit.operations.locations.Track; 010import jmri.jmrit.operations.rollingstock.RollingStockManager; 011import jmri.jmrit.operations.routes.Route; 012import jmri.jmrit.operations.routes.RouteLocation; 013import jmri.jmrit.operations.setup.OperationsSetupXml; 014import jmri.jmrit.operations.setup.Setup; 015import jmri.jmrit.operations.trains.Train; 016import jmri.jmrit.operations.trains.TrainManifestHeaderText; 017 018import org.jdom2.Element; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022/** 023 * Manages the cars. 024 * 025 * @author Daniel Boudreau Copyright (C) 2008, 2026 026 */ 027public class CarManager extends RollingStockManager<Car> implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize { 028 029 public CarManager() { 030 } 031 032 /** 033 * Finds an existing Car or creates a new Car if needed requires car's road 034 * and number 035 * 036 * @param road car road 037 * @param number car number 038 * @return new car or existing Car 039 */ 040 @Override 041 public Car newRS(String road, String number) { 042 Car car = getByRoadAndNumber(road, number); 043 if (car == null) { 044 car = new Car(road, number); 045 register(car); 046 } 047 return car; 048 } 049 050 @Override 051 public void deregister(Car car) { 052 super.deregister(car); 053 InstanceManager.getDefault(CarManagerXml.class).setDirty(true); 054 } 055 056 /** 057 * Sort by rolling stock location 058 * 059 * @return list of cars ordered by the Car's location 060 */ 061 @Override 062 public List<Car> getByLocationList() { 063 List<Car> byFinal = getByList(getByNumberList(), BY_FINAL_DEST); 064 List<Car> byKernel = getByList(byFinal, BY_KERNEL); 065 return getByList(byKernel, BY_LOCATION); 066 } 067 068 /** 069 * Sort by car kernel names 070 * 071 * @return list of cars ordered by car kernel 072 */ 073 public List<Car> getByKernelList() { 074 return getByList(getByList(getByNumberList(), BY_BLOCKING), BY_KERNEL); 075 } 076 077 /** 078 * Sort by car loads 079 * 080 * @return list of cars ordered by car loads 081 */ 082 public List<Car> getByLoadList() { 083 return getByList(getByLocationList(), BY_LOAD); 084 } 085 086 /** 087 * Sort by car return when empty location and track 088 * 089 * @return list of cars ordered by car return when empty 090 */ 091 public List<Car> getByRweList() { 092 return getByList(getByLocationList(), BY_RWE); 093 } 094 095 public List<Car> getByRwlList() { 096 return getByList(getByLocationList(), BY_RWL); 097 } 098 099 public List<Car> getByRouteList() { 100 return getByList(getByLocationList(), BY_ROUTE); 101 } 102 103 public List<Car> getByDivisionList() { 104 return getByList(getByLocationList(), BY_DIVISION); 105 } 106 107 public List<Car> getByFinalDestinationList() { 108 return getByList(getByDestinationList(), BY_FINAL_DEST); 109 } 110 111 /** 112 * Sort by car wait count 113 * 114 * @return list of cars ordered by wait count 115 */ 116 public List<Car> getByWaitList() { 117 return getByList(getByIdList(), BY_WAIT); 118 } 119 120 @Override 121 public List<Car> getByPickupList() { 122 return getByList(getByDestinationList(), BY_PICKUP); 123 } 124 125 // The special sort options for cars 126 private static final int BY_LOAD_TYPE = 29; 127 private static final int BY_LOAD = 30; 128 private static final int BY_KERNEL = 31; 129 private static final int BY_RWE = 32; // Return When Empty 130 private static final int BY_FINAL_DEST = 33; 131 private static final int BY_WAIT = 34; 132 private static final int BY_PICKUP = 35; 133 private static final int BY_HAZARD = 36; 134 private static final int BY_RWL = 37; // Return When loaded 135 private static final int BY_ROUTE = 38; 136 private static final int BY_DIVISION = 39; 137 138 // the name of the location and track is "split" 139 private static final int BY_SPLIT_FINAL_DEST = 40; 140 private static final int BY_SPLIT_LOCATION = 41; 141 private static final int BY_SPLIT_DESTINATION = 42; 142 143 @Override 144 protected java.util.Comparator<Car> getComparator(int attribute) { 145 switch (attribute) { 146 case BY_LOAD_TYPE: 147 // load types "Empty" and "Load", load goes before empty 148 return (c1, c2) -> (c2.getLoadType().compareToIgnoreCase(c1.getLoadType())); 149 case BY_LOAD: 150 return (c1, c2) -> (c1.getLoadName().compareToIgnoreCase(c2.getLoadName())); 151 case BY_KERNEL: 152 return (c1, c2) -> (c1.getKernelName().compareToIgnoreCase(c2.getKernelName())); 153 case BY_RWE: 154 return (c1, c2) -> (c1.getReturnWhenEmptyDestinationName() + c1.getReturnWhenEmptyDestTrackName()) 155 .compareToIgnoreCase( 156 c2.getReturnWhenEmptyDestinationName() + c2.getReturnWhenEmptyDestTrackName()); 157 case BY_RWL: 158 return (c1, c2) -> (c1.getReturnWhenLoadedDestinationName() + c1.getReturnWhenLoadedDestTrackName()) 159 .compareToIgnoreCase( 160 c2.getReturnWhenLoadedDestinationName() + c2.getReturnWhenLoadedDestTrackName()); 161 case BY_FINAL_DEST: 162 return (c1, c2) -> (c1.getFinalDestinationName() + c1.getFinalDestinationTrackName()) 163 .compareToIgnoreCase(c2.getFinalDestinationName() + c2.getFinalDestinationTrackName()); 164 case BY_ROUTE: 165 return (c1, c2) -> (c1.getRoutePath().compareToIgnoreCase(c2.getRoutePath())); 166 case BY_DIVISION: 167 return (c1, c2) -> (c1.getDivisionName().compareToIgnoreCase(c2.getDivisionName())); 168 case BY_WAIT: 169 return (c1, c2) -> (c1.getWait() - c2.getWait()); 170 case BY_PICKUP: 171 return (c1, c2) -> (c1.getPickupScheduleName().compareToIgnoreCase(c2.getPickupScheduleName())); 172 case BY_HAZARD: 173 return (c1, c2) -> ((c1.isHazardous() ? 1 : 0) - (c2.isHazardous() ? 1 : 0)); 174 case BY_SPLIT_FINAL_DEST: 175 return (c1, c2) -> (c1.getSplitFinalDestinationName() + c1.getSplitFinalDestinationTrackName()) 176 .compareToIgnoreCase( 177 c2.getSplitFinalDestinationName() + c2.getSplitFinalDestinationTrackName()); 178 case BY_SPLIT_LOCATION: 179 return (c1, c2) -> (c1.getStatus() + c1.getSplitLocationName() + c1.getSplitTrackName()) 180 .compareToIgnoreCase(c2.getStatus() + c2.getSplitLocationName() + c2.getSplitTrackName()); 181 case BY_SPLIT_DESTINATION: 182 return (c1, c2) -> (c1.getSplitDestinationName() + c1.getSplitDestinationTrackName()) 183 .compareToIgnoreCase(c2.getSplitDestinationName() + c2.getSplitDestinationTrackName()); 184 default: 185 return super.getComparator(attribute); 186 } 187 } 188 189 /** 190 * Return a list available cars (no assigned train or car already assigned 191 * to this train) on a route, cars are ordered least recently moved to most 192 * recently moved. Note that it is possible for a car to have a location, 193 * but no track assignment. 194 * 195 * @param train The Train to use. 196 * @return List of cars with no assigned train on a route 197 */ 198 public List<Car> getAvailableTrainList(Train train) { 199 List<Car> out = new ArrayList<>(); 200 Route route = train.getRoute(); 201 if (route == null) { 202 return out; 203 } 204 // get a list of locations served by this route 205 List<RouteLocation> routeList = route.getLocationsBySequenceList(); 206 // don't include Car at route destination 207 RouteLocation rlDestination = null; 208 if (routeList.size() > 1) { 209 rlDestination = routeList.get(routeList.size() - 1); 210 // However, if the destination is visited more than once, must 211 // include all cars 212 for (int i = 0; i < routeList.size() - 1; i++) { 213 if (rlDestination.getName().equals(routeList.get(i).getName())) { 214 rlDestination = null; // include cars at destination 215 break; 216 } 217 } 218 // local moves allowed at destination? Don't include cars in staging 219 if (rlDestination != null && 220 rlDestination.isLocalMovesAllowed() && 221 rlDestination.getLocation() != null && 222 !rlDestination.getLocation().isStaging()) { 223 rlDestination = null; // include cars at destination 224 } 225 } 226 // get rolling stock by track priority, load priority and then by moves 227 List<Car> sortByPriority = sortByTrackPriority(sortByLoadPriority(getByMovesList())); 228 // now build list of available cars for this route 229 for (Car car : sortByPriority) { 230 // only use cars with a location 231 if (car.getLocation() == null) { 232 continue; 233 } 234 RouteLocation rl = route.getLastLocationByName(car.getLocationName()); 235 // only allow cars that don't have an assigned train, or the 236 // assigned train is this one 237 if (rl != null && rl != rlDestination && (car.getTrain() == null || train.equals(car.getTrain()))) { 238 out.add(car); 239 } 240 } 241 return out; 242 } 243 244 // sorts the high priority cars to the start of the list 245 protected List<Car> sortByLoadPriority(List<Car> list) { 246 List<Car> out = new ArrayList<>(); 247 // move high priority cars to the start 248 for (Car car : list) { 249 if (car.getLoadPriority().equals(CarLoad.PRIORITY_HIGH)) { 250 out.add(car); 251 } 252 } 253 for (Car car : list) { 254 if (car.getLoadPriority().equals(CarLoad.PRIORITY_MEDIUM)) { 255 out.add(car); 256 } 257 } 258 // now load all of the remaining low priority cars 259 for (Car car : list) { 260 if (!out.contains(car)) { 261 out.add(car); 262 } 263 } 264 return out; 265 } 266 267 /** 268 * Provides a very sorted list of cars assigned to the train. Note that this 269 * isn't the final sort as the cars must be sorted by each location the 270 * train visits. 271 * <p> 272 * The sort priority is as follows: 273 * <ol> 274 * <li>Caboose or car with FRED to the end of the list, unless passenger. 275 * <li>Passenger cars have blocking numbers which places them relative to 276 * each other. Passenger cars with positive blocking numbers to the end of 277 * the list, but before cabooses or car with FRED. Passenger cars with 278 * negative blocking numbers are placed at the front of the train. 279 * <li>Car's destination (alphabetical by location and track name or by 280 * track blocking order) 281 * <li>Car is hazardous (hazardous placed after a non-hazardous car) 282 * <li>Car's current location (alphabetical by location and track name) 283 * <li>Car's final destination (alphabetical by location and track name) 284 * </ol> 285 * <p> 286 * Cars in a kernel are placed together by their kernel blocking numbers, 287 * except if they are type passenger. The kernel's position in the list is 288 * based on the lead car in the kernel. 289 * <p> 290 * If the train is to be blocked by track blocking order, all of the tracks 291 * at that location need a blocking number greater than 0. 292 * 293 * @param train The selected Train. 294 * @return Ordered list of cars assigned to the train 295 */ 296 public List<Car> getByTrainDestinationList(Train train) { 297 List<Car> byLoadType = getByList(getList(train), BY_LOAD_TYPE); 298 List<Car> byFinal = getByList(byLoadType, BY_SPLIT_FINAL_DEST); 299 List<Car> byLocation = getByList(byFinal, BY_SPLIT_LOCATION); 300 List<Car> byHazard = getByList(byLocation, BY_HAZARD); 301 List<Car> byDestination = getByList(byHazard, BY_SPLIT_DESTINATION); 302 // now place cabooses, cars with FRED, and passenger cars at the rear of the 303 // train 304 List<Car> out = new ArrayList<>(); 305 int lastCarsIndex = 0; // incremented each time a car is added to the end of the list 306 for (Car car : byDestination) { 307 if (car.getKernel() != null && !car.isLead() && !car.isPassenger()) { 308 continue; // not the lead car, skip for now. 309 } 310 if (!car.isCaboose() && !car.hasFred() && !car.isPassenger()) { 311 // sort order based on train direction when serving track, low to high if West 312 // or North bound trains 313 if (car.getDestinationTrack() != null && car.getDestinationTrack().getBlockingOrder() > 0) { 314 for (int j = 0; j < out.size(); j++) { 315 if (out.get(j).getDestinationTrack() == null) { 316 continue; 317 } 318 if (car.getRouteDestination() != null && 319 (car.getRouteDestination().getTrainDirectionString().equals(RouteLocation.WEST_DIR) || 320 car.getRouteDestination().getTrainDirectionString() 321 .equals(RouteLocation.NORTH_DIR))) { 322 if (car.getDestinationTrack().getBlockingOrder() < out.get(j).getDestinationTrack() 323 .getBlockingOrder()) { 324 out.add(j, car); 325 break; 326 } 327 // Train is traveling East or South when setting out the car 328 } else { 329 if (car.getDestinationTrack().getBlockingOrder() > out.get(j).getDestinationTrack() 330 .getBlockingOrder()) { 331 out.add(j, car); 332 break; 333 } 334 } 335 } 336 } 337 if (!out.contains(car)) { 338 out.add(out.size() - lastCarsIndex, car); 339 } 340 } else if (car.isPassenger()) { 341 if (car.getBlocking() < 0) { 342 // block passenger cars with negative blocking numbers at 343 // front of train 344 int index; 345 for (index = 0; index < out.size(); index++) { 346 Car carTest = out.get(index); 347 if (!carTest.isPassenger() || carTest.getBlocking() > car.getBlocking()) { 348 break; 349 } 350 } 351 out.add(index, car); 352 } else { 353 // block passenger cars at end of list, but before cabooses 354 // or car with FRED 355 int index; 356 for (index = 0; index < lastCarsIndex; index++) { 357 Car carTest = out.get(out.size() - 1 - index); 358 log.debug("Car ({}) has blocking number: {}", carTest.toString(), carTest.getBlocking()); 359 if (carTest.isPassenger() && 360 !carTest.isCaboose() && 361 !carTest.hasFred() && 362 carTest.getBlocking() < car.getBlocking()) { 363 break; 364 } 365 } 366 out.add(out.size() - index, car); 367 lastCarsIndex++; 368 } 369 } else if (car.isCaboose() || car.hasFred()) { 370 out.add(car); // place at end of list 371 lastCarsIndex++; 372 } 373 // group the cars in the kernel together, except passenger 374 if (car.isLead()) { 375 int index = out.indexOf(car); 376 int numberOfCars = 1; // already added the lead car to the list 377 for (Car kcar : car.getKernel().getCars()) { 378 if (car != kcar && !kcar.isPassenger()) { 379 // Block cars in kernel 380 for (int j = 0; j < numberOfCars; j++) { 381 if (kcar.getBlocking() < out.get(index + j).getBlocking()) { 382 out.add(index + j, kcar); 383 break; 384 } 385 } 386 if (!out.contains(kcar)) { 387 out.add(index + numberOfCars, kcar); 388 } 389 numberOfCars++; 390 if (car.hasFred() || car.isCaboose() || car.isPassenger() && car.getBlocking() > 0) { 391 lastCarsIndex++; // place entire kernel at the end of list 392 } 393 } 394 } 395 } 396 } 397 return out; 398 } 399 400 /** 401 * Get a list of car road names where the car was flagged as a caboose. 402 * 403 * @return List of caboose road names. 404 */ 405 public List<String> getCabooseRoadNames() { 406 List<String> names = new ArrayList<>(); 407 Enumeration<String> en = _hashTable.keys(); 408 while (en.hasMoreElements()) { 409 Car car = getById(en.nextElement()); 410 if (car.isCaboose() && !names.contains(car.getRoadName())) { 411 names.add(car.getRoadName()); 412 } 413 } 414 java.util.Collections.sort(names); 415 return names; 416 } 417 418 /** 419 * Get a list of car road names where the car was flagged with FRED 420 * 421 * @return List of road names of cars with FREDs 422 */ 423 public List<String> getFredRoadNames() { 424 List<String> names = new ArrayList<>(); 425 Enumeration<String> en = _hashTable.keys(); 426 while (en.hasMoreElements()) { 427 Car car = getById(en.nextElement()); 428 if (car.hasFred() && !names.contains(car.getRoadName())) { 429 names.add(car.getRoadName()); 430 } 431 } 432 java.util.Collections.sort(names); 433 return names; 434 } 435 436 /** 437 * Replace car loads 438 * 439 * @param type type of car 440 * @param oldLoadName old load name 441 * @param newLoadName new load name 442 */ 443 public void replaceLoad(String type, String oldLoadName, String newLoadName) { 444 List<Car> cars = getList(); 445 for (Car car : cars) { 446 if (car.getTypeName().equals(type) && car.getLoadName().equals(oldLoadName)) { 447 if (newLoadName != null) { 448 car.setLoadName(newLoadName); 449 } else { 450 car.setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName()); 451 } 452 } 453 if (car.getTypeName().equals(type) && car.getReturnWhenEmptyLoadName().equals(oldLoadName)) { 454 if (newLoadName != null) { 455 car.setReturnWhenEmptyLoadName(newLoadName); 456 } else { 457 car.setReturnWhenEmptyLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName()); 458 } 459 } 460 if (car.getTypeName().equals(type) && car.getReturnWhenLoadedLoadName().equals(oldLoadName)) { 461 if (newLoadName != null) { 462 car.setReturnWhenLoadedLoadName(newLoadName); 463 } else { 464 car.setReturnWhenLoadedLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultLoadName()); 465 } 466 } 467 } 468 } 469 470 public List<Car> getCarsLocationUnknown() { 471 List<Car> mias = new ArrayList<>(); 472 for (Car car : getByIdList()) { 473 if (car.isLocationUnknown()) { 474 mias.add(car); // return unknown location car 475 } 476 } 477 return mias; 478 } 479 480 /** 481 * Determines a car's weight in ounces based on car's scale length 482 * 483 * @param carLength Car's scale length 484 * @return car's weight in ounces 485 * @throws NumberFormatException if length isn't a number 486 */ 487 public static String calculateCarWeight(String carLength) throws NumberFormatException { 488 double doubleCarLength = Double.parseDouble(carLength) * 12 / Setup.getScaleRatio(); 489 double doubleCarWeight = (Setup.getInitalWeight() + doubleCarLength * Setup.getAddWeight()) / 1000; 490 NumberFormat nf = NumberFormat.getNumberInstance(); 491 nf.setMaximumFractionDigits(1); 492 return nf.format(doubleCarWeight); // car weight in ounces. 493 } 494 495 /** 496 * Used to determine if any car has been assigned a division 497 * 498 * @return true if any car has been assigned a division, otherwise false 499 */ 500 public boolean isThereDivisions() { 501 for (Car car : getList()) { 502 if (car.getDivision() != null) { 503 return true; 504 } 505 } 506 return false; 507 } 508 509 /** 510 * Used to determine if there are clone cars. 511 * 512 * @return true if there are clone cars, otherwise false. 513 */ 514 public boolean isThereClones() { 515 for (Car car : getList()) { 516 if (car.isClone()) { 517 return true; 518 } 519 } 520 return false; 521 } 522 523 /** 524 * Creates a clone for the car, and clones if the car is part of a kernel. 525 * Note that a car have have multiple clones. 526 * 527 * @param car The car to clone 528 * @param track The destination track for the clones 529 * @param train The train transporting the clones 530 * @param startTime The date and time the clones were moved 531 * @return clone for this car 532 */ 533 public Car createClone(Car car, Track track, Train train, LocalDateTime startTime) { 534 Car clone = createClone(car); 535 // for reset 536 clone.setPreviousFinalDestination(car.getPreviousFinalDestination()); 537 clone.setPreviousFinalDestinationTrack(car.getPreviousFinalDestinationTrack()); 538 clone.setPreviousScheduleId(car.getScheduleItemId()); 539 createCloneKernel(car, track, train, startTime, clone); 540 // move car to new location for later pick up 541 finshCreateClone(car, track, train, startTime, clone); 542 return clone; 543 } 544 545 private void createCloneKernel(Car car, Track track, Train train, LocalDateTime startTime, Car cloneCar) { 546 if (car.getKernel() != null) { 547 String kernelName = car.getKernelName() + Car.CLONE + padNumber(car.getCloneOrder()); 548 Kernel kernel = InstanceManager.getDefault(KernelManager.class).newKernel(kernelName); 549 cloneCar.setKernel(kernel); 550 for (Car kar : car.getKernel().getCars()) { 551 if (kar != car) { 552 Car nClone = createClone(kar, car.getCloneOrder()); 553 nClone.setKernel(kernel); 554 // for reset 555 nClone.setPreviousFinalDestination(car.getPreviousFinalDestination()); 556 nClone.setPreviousFinalDestinationTrack(car.getPreviousFinalDestinationTrack()); 557 // move car to new location for later pick up 558 finshCreateClone(kar, track, train, startTime, nClone); 559 } 560 } 561 } 562 } 563 564 int _commentLength = 0; 565 566 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST", 567 justification = "I18N of Info Message") 568 public int getMaxCommentLength() { 569 if (_commentLength == 0) { 570 _commentLength = TrainManifestHeaderText.getStringHeader_Comment().length(); 571 String comment = ""; 572 Car carMax = null; 573 for (Car car : getList()) { 574 if (car.getComment().length() > _commentLength) { 575 _commentLength = car.getComment().length(); 576 comment = car.getComment(); 577 carMax = car; 578 } 579 } 580 if (carMax != null) { 581 log.info(Bundle.getMessage("InfoMaxComment", carMax.toString(), comment, _commentLength)); 582 } 583 } 584 return _commentLength; 585 } 586 587 public void load(Element root) { 588 if (root.getChild(Xml.CARS) != null) { 589 List<Element> eCars = root.getChild(Xml.CARS).getChildren(Xml.CAR); 590 log.debug("readFile sees {} cars", eCars.size()); 591 for (Element eCar : eCars) { 592 register(new Car(eCar)); 593 } 594 } 595 } 596 597 /** 598 * Create an XML element to represent this Entry. This member has to remain 599 * synchronized with the detailed DTD in operations-cars.dtd. 600 * 601 * @param root The common Element for operations-cars.dtd. 602 */ 603 public void store(Element root) { 604 // nothing to save under options 605 root.addContent(new Element(Xml.OPTIONS)); 606 607 Element values; 608 root.addContent(values = new Element(Xml.CARS)); 609 // add entries 610 List<Car> carList = getByIdList(); 611 for (Car rs : carList) { 612 Car car = rs; 613 values.addContent(car.store()); 614 } 615 } 616 617 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 618 // Set dirty 619 InstanceManager.getDefault(CarManagerXml.class).setDirty(true); 620 super.firePropertyChange(p, old, n); 621 } 622 623 @Override 624 public void propertyChange(PropertyChangeEvent evt) { 625 if (evt.getPropertyName().equals(Car.COMMENT_CHANGED_PROPERTY)) { 626 _commentLength = 0; 627 } 628 super.propertyChange(evt); 629 } 630 631 private static final Logger log = LoggerFactory.getLogger(CarManager.class); 632 633 @Override 634 public void initialize() { 635 InstanceManager.getDefault(OperationsSetupXml.class); // load setup 636 // create manager to load cars and their attributes 637 InstanceManager.getDefault(CarManagerXml.class); 638 } 639 640}