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