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