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