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