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 025 */ 026public class CarManager extends RollingStockManager<Car> 027 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 and 034 * 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 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 public List<Car> getCarsUsingTrack(Track track) { 476 List<Car> list = new ArrayList<>(); 477 for (Car car : getByIdList()) { 478 if (car.getTrack() == track) { 479 list.add(car); 480 } 481 } 482 return list; 483 } 484 485 /** 486 * Determines a car's weight in ounces based on car's scale length 487 * 488 * @param carLength Car's scale length 489 * @return car's weight in ounces 490 * @throws NumberFormatException if length isn't a number 491 */ 492 public static String calculateCarWeight(String carLength) throws NumberFormatException { 493 double doubleCarLength = Double.parseDouble(carLength) * 12 / Setup.getScaleRatio(); 494 double doubleCarWeight = (Setup.getInitalWeight() + doubleCarLength * Setup.getAddWeight()) / 1000; 495 NumberFormat nf = NumberFormat.getNumberInstance(); 496 nf.setMaximumFractionDigits(1); 497 return nf.format(doubleCarWeight); // car weight in ounces. 498 } 499 500 /** 501 * Used to determine if any car has been assigned a division 502 * 503 * @return true if any car has been assigned a division, otherwise false 504 */ 505 public boolean isThereDivisions() { 506 for (Car car : getList()) { 507 if (car.getDivision() != null) { 508 return true; 509 } 510 } 511 return false; 512 } 513 514 /** 515 * Used to determine if there are clone cars. 516 * 517 * @return true if there are clone cars, otherwise false. 518 */ 519 public boolean isThereClones() { 520 for (Car car : getList()) { 521 if (car.isClone()) { 522 return true; 523 } 524 } 525 return false; 526 } 527 528 public Car createClone(Car car) { 529 int cloneCreationOrder = getCloneCreationOrder(); 530 Car cloneCar = car.copy(); 531 cloneCar.setNumber(car.getNumber() + Car.CLONE + cloneCreationOrder); 532 cloneCar.setClone(true); 533 // register car before setting location so the car gets logged 534 register(cloneCar); 535 return cloneCar; 536 } 537 538 int cloneCreationOrder = 0; 539 540 /** 541 * Returns the highest clone creation order given to a clone. 542 * 543 * @return 1 if the first clone created, otherwise the highest found plus 544 * one. Automatically increments. 545 */ 546 private int getCloneCreationOrder() { 547 if (cloneCreationOrder == 0) { 548 for (Car car : getList()) { 549 if (car.isClone()) { 550 String[] number = car.getNumber().split(Car.CLONE_REGEX); 551 int creationOrder = Integer.parseInt(number[1]); 552 if (creationOrder > cloneCreationOrder) { 553 cloneCreationOrder = creationOrder; 554 } 555 } 556 } 557 } 558 return ++cloneCreationOrder; 559 } 560 561 /** 562 * Returns the car's last clone car if there's one. 563 * @param car The car searching for a clone 564 * @return Returns the car's last clone car, null if there isn't a clone car. 565 */ 566 public Car getClone(Car car) { 567 List<Car> cars = getByLastDateList(); 568 // clone with the highest creation number will be last in the list 569 for (int i = cars.size() - 1; i >= 0; i--) { 570 Car kar = cars.get(i); 571 if (kar.isClone() && 572 kar.getDestinationTrack() == car.getTrack() && 573 kar.getRoadName().equals(car.getRoadName()) && 574 kar.getNumber().split(Car.CLONE_REGEX)[0].equals(car.getNumber())) { 575 return kar; 576 } 577 } 578 return null; // no clone for this car 579 } 580 581 int _commentLength = 0; 582 583 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 584 justification="I18N of Info Message") 585 public int getMaxCommentLength() { 586 if (_commentLength == 0) { 587 _commentLength = TrainManifestHeaderText.getStringHeader_Comment().length(); 588 String comment = ""; 589 Car carMax = null; 590 for (Car car : getList()) { 591 if (car.getComment().length() > _commentLength) { 592 _commentLength = car.getComment().length(); 593 comment = car.getComment(); 594 carMax = car; 595 } 596 } 597 if (carMax != null) { 598 log.info(Bundle.getMessage("InfoMaxComment", carMax.toString(), comment, _commentLength)); 599 } 600 } 601 return _commentLength; 602 } 603 604 public void load(Element root) { 605 if (root.getChild(Xml.CARS) != null) { 606 List<Element> eCars = root.getChild(Xml.CARS).getChildren(Xml.CAR); 607 log.debug("readFile sees {} cars", eCars.size()); 608 for (Element eCar : eCars) { 609 register(new Car(eCar)); 610 } 611 } 612 } 613 614 /** 615 * Create an XML element to represent this Entry. This member has to remain 616 * synchronized with the detailed DTD in operations-cars.dtd. 617 * 618 * @param root The common Element for operations-cars.dtd. 619 */ 620 public void store(Element root) { 621 // nothing to save under options 622 root.addContent(new Element(Xml.OPTIONS)); 623 624 Element values; 625 root.addContent(values = new Element(Xml.CARS)); 626 // add entries 627 List<Car> carList = getByIdList(); 628 for (Car rs : carList) { 629 Car car = rs; 630 values.addContent(car.store()); 631 } 632 } 633 634 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 635 // Set dirty 636 InstanceManager.getDefault(CarManagerXml.class).setDirty(true); 637 super.firePropertyChange(p, old, n); 638 } 639 640 @Override 641 public void propertyChange(PropertyChangeEvent evt) { 642 if (evt.getPropertyName().equals(Car.COMMENT_CHANGED_PROPERTY)) { 643 _commentLength = 0; 644 } 645 super.propertyChange(evt); 646 } 647 648 private final static Logger log = LoggerFactory.getLogger(CarManager.class); 649 650 @Override 651 public void initialize() { 652 InstanceManager.getDefault(OperationsSetupXml.class); // load setup 653 // create manager to load cars and their attributes 654 InstanceManager.getDefault(CarManagerXml.class); 655 } 656 657}