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 protected List<Car> sortByTrackPriority(List<Car> list) { 264 List<Car> out = new ArrayList<>(); 265 // sort cars by track priority 266 for (Car car : list) { 267 if (car.getTrack() != null && car.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) { 268 out.add(car); 269 } 270 } 271 for (Car car : list) { 272 if (car.getTrack() != null && car.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) { 273 out.add(car); 274 } 275 } 276 for (Car car : list) { 277 if (car.getTrack() != null && car.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) { 278 out.add(car); 279 } 280 } 281 for (Car car : list) { 282 if (car.getTrack() != null && car.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) { 283 out.add(car); 284 } 285 } 286 // cars without a track assignment 287 for (Car car : list) { 288 if (!out.contains(car)) { 289 out.add(car); 290 } 291 } 292 return out; 293 } 294 295 /** 296 * Provides a very sorted list of cars assigned to the train. Note that this 297 * isn't the final sort as the cars must be sorted by each location the 298 * train visits. 299 * <p> 300 * The sort priority is as follows: 301 * <ol> 302 * <li>Caboose or car with FRED to the end of the list, unless passenger. 303 * <li>Passenger cars have blocking numbers which places them relative to 304 * each other. Passenger cars with positive blocking numbers to the end of 305 * the list, but before cabooses or car with FRED. Passenger cars with 306 * negative blocking numbers are placed at the front of the train. 307 * <li>Car's destination (alphabetical by location and track name or by 308 * track blocking order) 309 * <li>Car is hazardous (hazardous placed after a non-hazardous car) 310 * <li>Car's current location (alphabetical by location and track name) 311 * <li>Car's final destination (alphabetical by location and track name) 312 * </ol> 313 * <p> 314 * Cars in a kernel are placed together by their kernel blocking numbers, 315 * except if they are type passenger. The kernel's position in the list is 316 * based on the lead car in the kernel. 317 * <p> 318 * If the train is to be blocked by track blocking order, all of the tracks 319 * at that location need a blocking number greater than 0. 320 * 321 * @param train The selected Train. 322 * @return Ordered list of cars assigned to the train 323 */ 324 public List<Car> getByTrainDestinationList(Train train) { 325 List<Car> byFinal = getByList(getList(train), BY_SPLIT_FINAL_DEST); 326 List<Car> byLocation = getByList(byFinal, BY_SPLIT_LOCATION); 327 List<Car> byHazard = getByList(byLocation, BY_HAZARD); 328 List<Car> byDestination = getByList(byHazard, BY_SPLIT_DESTINATION); 329 // now place cabooses, cars with FRED, and passenger cars at the rear of the 330 // train 331 List<Car> out = new ArrayList<>(); 332 int lastCarsIndex = 0; // incremented each time a car is added to the end of the list 333 for (Car car : byDestination) { 334 if (car.getKernel() != null && !car.isLead() && !car.isPassenger()) { 335 continue; // not the lead car, skip for now. 336 } 337 if (!car.isCaboose() && !car.hasFred() && !car.isPassenger()) { 338 // sort order based on train direction when serving track, low to high if West 339 // or North bound trains 340 if (car.getDestinationTrack() != null && car.getDestinationTrack().getBlockingOrder() > 0) { 341 for (int j = 0; j < out.size(); j++) { 342 if (out.get(j).getDestinationTrack() == null) { 343 continue; 344 } 345 if (car.getRouteDestination() != null && 346 (car.getRouteDestination().getTrainDirectionString().equals(RouteLocation.WEST_DIR) || 347 car.getRouteDestination().getTrainDirectionString() 348 .equals(RouteLocation.NORTH_DIR))) { 349 if (car.getDestinationTrack().getBlockingOrder() < out.get(j).getDestinationTrack() 350 .getBlockingOrder()) { 351 out.add(j, car); 352 break; 353 } 354 // Train is traveling East or South when setting out the car 355 } else { 356 if (car.getDestinationTrack().getBlockingOrder() > out.get(j).getDestinationTrack() 357 .getBlockingOrder()) { 358 out.add(j, car); 359 break; 360 } 361 } 362 } 363 } 364 if (!out.contains(car)) { 365 out.add(out.size() - lastCarsIndex, car); 366 } 367 } else if (car.isPassenger()) { 368 if (car.getBlocking() < 0) { 369 // block passenger cars with negative blocking numbers at 370 // front of train 371 int index; 372 for (index = 0; index < out.size(); index++) { 373 Car carTest = out.get(index); 374 if (!carTest.isPassenger() || carTest.getBlocking() > car.getBlocking()) { 375 break; 376 } 377 } 378 out.add(index, car); 379 } else { 380 // block passenger cars at end of list, but before cabooses 381 // or car with FRED 382 int index; 383 for (index = 0; index < lastCarsIndex; index++) { 384 Car carTest = out.get(out.size() - 1 - index); 385 log.debug("Car ({}) has blocking number: {}", carTest.toString(), carTest.getBlocking()); 386 if (carTest.isPassenger() && 387 !carTest.isCaboose() && 388 !carTest.hasFred() && 389 carTest.getBlocking() < car.getBlocking()) { 390 break; 391 } 392 } 393 out.add(out.size() - index, car); 394 lastCarsIndex++; 395 } 396 } else if (car.isCaboose() || car.hasFred()) { 397 out.add(car); // place at end of list 398 lastCarsIndex++; 399 } 400 // group the cars in the kernel together, except passenger 401 if (car.isLead()) { 402 int index = out.indexOf(car); 403 int numberOfCars = 1; // already added the lead car to the list 404 for (Car kcar : car.getKernel().getCars()) { 405 if (car != kcar && !kcar.isPassenger()) { 406 // Block cars in kernel 407 for (int j = 0; j < numberOfCars; j++) { 408 if (kcar.getBlocking() < out.get(index + j).getBlocking()) { 409 out.add(index + j, kcar); 410 break; 411 } 412 } 413 if (!out.contains(kcar)) { 414 out.add(index + numberOfCars, kcar); 415 } 416 numberOfCars++; 417 if (car.hasFred() || car.isCaboose() || car.isPassenger() && car.getBlocking() > 0) { 418 lastCarsIndex++; // place entire kernel at the end of list 419 } 420 } 421 } 422 } 423 } 424 return out; 425 } 426 427 /** 428 * Get a list of car road names where the car was flagged as a caboose. 429 * 430 * @return List of caboose road names. 431 */ 432 public List<String> getCabooseRoadNames() { 433 List<String> names = new ArrayList<>(); 434 Enumeration<String> en = _hashTable.keys(); 435 while (en.hasMoreElements()) { 436 Car car = getById(en.nextElement()); 437 if (car.isCaboose() && !names.contains(car.getRoadName())) { 438 names.add(car.getRoadName()); 439 } 440 } 441 java.util.Collections.sort(names); 442 return names; 443 } 444 445 /** 446 * Get a list of car road names where the car was flagged with FRED 447 * 448 * @return List of road names of cars with FREDs 449 */ 450 public List<String> getFredRoadNames() { 451 List<String> names = new ArrayList<>(); 452 Enumeration<String> en = _hashTable.keys(); 453 while (en.hasMoreElements()) { 454 Car car = getById(en.nextElement()); 455 if (car.hasFred() && !names.contains(car.getRoadName())) { 456 names.add(car.getRoadName()); 457 } 458 } 459 java.util.Collections.sort(names); 460 return names; 461 } 462 463 /** 464 * Replace car loads 465 * 466 * @param type type of car 467 * @param oldLoadName old load name 468 * @param newLoadName new load name 469 */ 470 public void replaceLoad(String type, String oldLoadName, String newLoadName) { 471 List<Car> cars = getList(); 472 for (Car car : cars) { 473 if (car.getTypeName().equals(type) && car.getLoadName().equals(oldLoadName)) { 474 if (newLoadName != null) { 475 car.setLoadName(newLoadName); 476 } else { 477 car.setLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName()); 478 } 479 } 480 if (car.getTypeName().equals(type) && car.getReturnWhenEmptyLoadName().equals(oldLoadName)) { 481 if (newLoadName != null) { 482 car.setReturnWhenEmptyLoadName(newLoadName); 483 } else { 484 car.setReturnWhenEmptyLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultEmptyName()); 485 } 486 } 487 if (car.getTypeName().equals(type) && car.getReturnWhenLoadedLoadName().equals(oldLoadName)) { 488 if (newLoadName != null) { 489 car.setReturnWhenLoadedLoadName(newLoadName); 490 } else { 491 car.setReturnWhenLoadedLoadName(InstanceManager.getDefault(CarLoads.class).getDefaultLoadName()); 492 } 493 } 494 } 495 } 496 497 public List<Car> getCarsLocationUnknown() { 498 List<Car> mias = new ArrayList<>(); 499 List<Car> cars = getByIdList(); 500 for (Car rs : cars) { 501 Car car = rs; 502 if (car.isLocationUnknown()) { 503 mias.add(car); // return unknown location car 504 } 505 } 506 return mias; 507 } 508 509 /** 510 * Determines a car's weight in ounces based on car's scale length 511 * 512 * @param carLength Car's scale length 513 * @return car's weight in ounces 514 * @throws NumberFormatException if length isn't a number 515 */ 516 public static String calculateCarWeight(String carLength) throws NumberFormatException { 517 double doubleCarLength = Double.parseDouble(carLength) * 12 / Setup.getScaleRatio(); 518 double doubleCarWeight = (Setup.getInitalWeight() + doubleCarLength * Setup.getAddWeight()) / 1000; 519 NumberFormat nf = NumberFormat.getNumberInstance(); 520 nf.setMaximumFractionDigits(1); 521 return nf.format(doubleCarWeight); // car weight in ounces. 522 } 523 524 /** 525 * Used to determine if any car has been assigned a division 526 * 527 * @return true if any car has been assigned a division, otherwise false 528 */ 529 public boolean isThereDivisions() { 530 for (Car car : getList()) { 531 if (car.getDivision() != null) { 532 return true; 533 } 534 } 535 return false; 536 } 537 538 /** 539 * Used to determine if there are clone cars. 540 * 541 * @return true if there are clone cars, otherwise false. 542 */ 543 public boolean isThereClones() { 544 for (Car car : getList()) { 545 if (car.isClone()) { 546 return true; 547 } 548 } 549 return false; 550 } 551 552 int cloneCreationOrder = 0; 553 554 /** 555 * Returns the highest clone creation order given to a clone. 556 * 557 * @return 1 if the first clone created, otherwise the highest found plus 558 * one. Automatically increments. 559 */ 560 public int getCloneCreationOrder() { 561 if (cloneCreationOrder == 0) { 562 for (Car car : getList()) { 563 if (car.isClone()) { 564 String[] number = car.getNumber().split(Car.CLONE_REGEX); 565 int creationOrder = Integer.parseInt(number[1]); 566 if (creationOrder > cloneCreationOrder) { 567 cloneCreationOrder = creationOrder; 568 } 569 } 570 } 571 } 572 return ++cloneCreationOrder; 573 } 574 575 int _commentLength = 0; 576 577 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 578 justification="I18N of Info Message") 579 public int getMaxCommentLength() { 580 if (_commentLength == 0) { 581 _commentLength = TrainManifestHeaderText.getStringHeader_Comment().length(); 582 String comment = ""; 583 Car carMax = null; 584 for (Car car : getList()) { 585 if (car.getComment().length() > _commentLength) { 586 _commentLength = car.getComment().length(); 587 comment = car.getComment(); 588 carMax = car; 589 } 590 } 591 if (carMax != null) { 592 log.info(Bundle.getMessage("InfoMaxComment", carMax.toString(), comment, _commentLength)); 593 } 594 } 595 return _commentLength; 596 } 597 598 public void load(Element root) { 599 if (root.getChild(Xml.CARS) != null) { 600 List<Element> eCars = root.getChild(Xml.CARS).getChildren(Xml.CAR); 601 log.debug("readFile sees {} cars", eCars.size()); 602 for (Element eCar : eCars) { 603 register(new Car(eCar)); 604 } 605 } 606 } 607 608 /** 609 * Create an XML element to represent this Entry. This member has to remain 610 * synchronized with the detailed DTD in operations-cars.dtd. 611 * 612 * @param root The common Element for operations-cars.dtd. 613 */ 614 public void store(Element root) { 615 // nothing to save under options 616 root.addContent(new Element(Xml.OPTIONS)); 617 618 Element values; 619 root.addContent(values = new Element(Xml.CARS)); 620 // add entries 621 List<Car> carList = getByIdList(); 622 for (Car rs : carList) { 623 Car car = rs; 624 values.addContent(car.store()); 625 } 626 } 627 628 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 629 // Set dirty 630 InstanceManager.getDefault(CarManagerXml.class).setDirty(true); 631 super.firePropertyChange(p, old, n); 632 } 633 634 @Override 635 public void propertyChange(PropertyChangeEvent evt) { 636 if (evt.getPropertyName().equals(Car.COMMENT_CHANGED_PROPERTY)) { 637 _commentLength = 0; 638 } 639 super.propertyChange(evt); 640 } 641 642 private final static Logger log = LoggerFactory.getLogger(CarManager.class); 643 644 @Override 645 public void initialize() { 646 InstanceManager.getDefault(OperationsSetupXml.class); // load setup 647 // create manager to load cars and their attributes 648 InstanceManager.getDefault(CarManagerXml.class); 649 } 650 651}