001package jmri.jmrit.operations.rollingstock; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.time.LocalDateTime; 006import java.util.*; 007 008import javax.annotation.OverridingMethodsMustInvokeSuper; 009 010import jmri.beans.PropertyChangeSupport; 011import jmri.jmrit.operations.locations.Location; 012import jmri.jmrit.operations.locations.Track; 013import jmri.jmrit.operations.trains.Train; 014import jmri.jmrit.operations.trains.trainbuilder.TrainCommon; 015 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018 019/** 020 * Base class for rolling stock managers car and engine. 021 * 022 * @author Daniel Boudreau Copyright (C) 2010, 2011, 2026 023 * @param <T> the type of RollingStock managed by this manager 024 */ 025public abstract class RollingStockManager<T extends RollingStock> extends PropertyChangeSupport implements PropertyChangeListener { 026 027 public static final String NONE = ""; 028 029 // RollingStock 030 protected Hashtable<String, T> _hashTable = new Hashtable<>(); 031 032 public static final String LISTLENGTH_CHANGED_PROPERTY = "RollingStockListLength"; // NOI18N 033 034 abstract public RollingStock newRS(String road, String number); 035 036 public RollingStockManager() { 037 } 038 039 /** 040 * Get the number of items in the roster 041 * 042 * @return Number of rolling stock in the Roster 043 */ 044 public int getNumEntries() { 045 return _hashTable.size(); 046 } 047 048 public void dispose() { 049 deleteAll(); 050 } 051 052 /** 053 * Get rolling stock by id 054 * 055 * @param id The string id. 056 * 057 * @return requested RollingStock object or null if none exists 058 */ 059 public T getById(String id) { 060 return _hashTable.get(id); 061 } 062 063 /** 064 * Get rolling stock by road and number 065 * 066 * @param road RollingStock road 067 * @param number RollingStock number 068 * @return requested RollingStock object or null if none exists 069 */ 070 public T getByRoadAndNumber(String road, String number) { 071 String id = RollingStock.createId(road, number); 072 return getById(id); 073 } 074 075 /** 076 * Get a rolling stock by type and road. Used to test that rolling stock 077 * with a specific type and road exists. 078 * 079 * @param type RollingStock type. 080 * @param road RollingStock road. 081 * @return the first RollingStock found with the specified type and road. 082 */ 083 public T getByTypeAndRoad(String type, String road) { 084 Enumeration<String> en = _hashTable.keys(); 085 while (en.hasMoreElements()) { 086 T rs = getById(en.nextElement()); 087 if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) { 088 return rs; 089 } 090 } 091 return null; 092 } 093 094 /** 095 * Get a rolling stock by Radio Frequency Identification (RFID) 096 * 097 * @param rfid RollingStock's RFID. 098 * @return the RollingStock with the specific RFID, or null if not found 099 */ 100 public T getByRfid(String rfid) { 101 Enumeration<String> en = _hashTable.keys(); 102 while (en.hasMoreElements()) { 103 T rs = getById(en.nextElement()); 104 if (rs.getRfid().equals(rfid)) { 105 return rs; 106 } 107 } 108 return null; 109 } 110 111 /** 112 * Load RollingStock. 113 * 114 * @param rs The RollingStock to load. 115 */ 116 public void register(T rs) { 117 if (!_hashTable.containsKey(rs.getId())) { 118 int oldSize = _hashTable.size(); 119 rs.addPropertyChangeListener(this); 120 _hashTable.put(rs.getId(), rs); 121 firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size()); 122 } else { 123 log.error("Duplicate rolling stock id: ({})", rs.getId()); 124 rs.dispose(); 125 } 126 } 127 128 /** 129 * Unload RollingStock. 130 * 131 * @param rs The RollingStock to delete. 132 */ 133 public void deregister(T rs) { 134 rs.removePropertyChangeListener(this); 135 rs.dispose(); 136 int oldSize = _hashTable.size(); 137 _hashTable.remove(rs.getId()); 138 firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size()); 139 } 140 141 /** 142 * Remove all RollingStock from roster 143 */ 144 public void deleteAll() { 145 int oldSize = _hashTable.size(); 146 Enumeration<String> en = _hashTable.keys(); 147 while (en.hasMoreElements()) { 148 T rs = getById(en.nextElement()); 149 rs.dispose(); 150 _hashTable.remove(rs.getId()); 151 } 152 firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size()); 153 } 154 155 public void resetMoves() { 156 resetMoves(getList()); 157 } 158 159 public void resetMoves(List<T> list) { 160 for (RollingStock rs : list) { 161 rs.setMoves(0); 162 } 163 } 164 165 /** 166 * Returns a list (no order) of RollingStock. 167 * 168 * @return list of RollingStock 169 */ 170 public List<T> getList() { 171 return new ArrayList<>(_hashTable.values()); 172 } 173 174 /** 175 * Sort by rolling stock id 176 * 177 * @return list of RollingStock ordered by id 178 */ 179 public List<T> getByIdList() { 180 Enumeration<String> en = _hashTable.keys(); 181 String[] arr = new String[_hashTable.size()]; 182 List<T> out = new ArrayList<>(); 183 int i = 0; 184 while (en.hasMoreElements()) { 185 arr[i++] = en.nextElement(); 186 } 187 Arrays.sort(arr); 188 for (i = 0; i < arr.length; i++) { 189 out.add(getById(arr[i])); 190 } 191 return out; 192 } 193 194 /** 195 * Sort by rolling stock road name 196 * 197 * @return list of RollingStock ordered by road name 198 */ 199 public List<T> getByRoadNameList() { 200 return getByList(getByIdList(), BY_ROAD); 201 } 202 203 private static final int PAGE_SIZE = 64; 204 private static final int NOT_INTEGER = -999999999; // flag when RS number isn't an Integer 205 206 /** 207 * Sort by rolling stock number, number can be alphanumeric. RollingStock 208 * number can also be in the format of nnnn-N, where the "-N" allows the 209 * user to enter RollingStock with similar numbers. 210 * 211 * @return list of RollingStock ordered by number 212 */ 213 public List<T> getByNumberList() { 214 // first get by road list 215 List<T> sortIn = getByRoadNameList(); 216 // now re-sort 217 List<T> out = new ArrayList<>(); 218 int rsNumber = 0; 219 int outRsNumber = 0; 220 221 for (T rs : sortIn) { 222 boolean rsAdded = false; 223 try { 224 rsNumber = Integer.parseInt(rs.getNumber()); 225 rs.number = rsNumber; 226 } catch (NumberFormatException e) { 227 // maybe rolling stock number in the format nnnn-N 228 try { 229 String[] number = rs.getNumber().split(TrainCommon.HYPHEN); 230 rsNumber = Integer.parseInt(number[0]); 231 rs.number = rsNumber; 232 } catch (NumberFormatException e2) { 233 rs.number = NOT_INTEGER; 234 // sort alphanumeric numbers at the end of the out list 235 String numberIn = rs.getNumber(); 236 // log.debug("rolling stock in road number ("+numberIn+") isn't a number"); 237 for (int k = (out.size() - 1); k >= 0; k--) { 238 String numberOut = out.get(k).getNumber(); 239 try { 240 Integer.parseInt(numberOut); 241 // done, place rolling stock with alphanumeric 242 // number after rolling stocks with real numbers. 243 out.add(k + 1, rs); 244 rsAdded = true; 245 break; 246 } catch (NumberFormatException e3) { 247 if (numberIn.compareToIgnoreCase(numberOut) >= 0) { 248 out.add(k + 1, rs); 249 rsAdded = true; 250 break; 251 } 252 } 253 } 254 if (!rsAdded) { 255 out.add(0, rs); 256 } 257 continue; 258 } 259 } 260 261 int start = 0; 262 // page to improve sort performance. 263 int divisor = out.size() / PAGE_SIZE; 264 for (int k = divisor; k > 0; k--) { 265 outRsNumber = out.get((out.size() - 1) * k / divisor).number; 266 if (outRsNumber == NOT_INTEGER) { 267 continue; 268 } 269 if (rsNumber >= outRsNumber) { 270 start = (out.size() - 1) * k / divisor; 271 break; 272 } 273 } 274 for (int j = start; j < out.size(); j++) { 275 outRsNumber = out.get(j).number; 276 if (outRsNumber == NOT_INTEGER) { 277 try { 278 outRsNumber = Integer.parseInt(out.get(j).getNumber()); 279 } catch (NumberFormatException e) { 280 try { 281 String[] number = out.get(j).getNumber().split(TrainCommon.HYPHEN); 282 outRsNumber = Integer.parseInt(number[0]); 283 } catch (NumberFormatException e2) { 284 // force add 285 outRsNumber = rsNumber + 1; 286 } 287 } 288 } 289 if (rsNumber < outRsNumber) { 290 out.add(j, rs); 291 rsAdded = true; 292 break; 293 } 294 } 295 if (!rsAdded) { 296 out.add(rs); 297 } 298 } 299 // log.debug("end rolling stock sort by number list"); 300 return out; 301 } 302 303 /** 304 * Sort by rolling stock type names 305 * 306 * @return list of RollingStock ordered by RollingStock type 307 */ 308 public List<T> getByTypeList() { 309 return getByList(getByRoadNameList(), BY_TYPE); 310 } 311 312 /** 313 * Return rolling stock of a specific type 314 * 315 * @param type type of rolling stock 316 * @return list of RollingStock that are specific type 317 */ 318 public List<T> getByTypeList(String type) { 319 List<T> typeList = getByTypeList(); 320 List<T> out = new ArrayList<>(); 321 for (T rs : typeList) { 322 if (rs.getTypeName().equals(type)) { 323 out.add(rs); 324 } 325 } 326 return out; 327 } 328 329 /** 330 * Sort by rolling stock color names 331 * 332 * @return list of RollingStock ordered by RollingStock color 333 */ 334 public List<T> getByColorList() { 335 return getByList(getByTypeList(), BY_COLOR); 336 } 337 338 /** 339 * Sort by rolling stock location 340 * 341 * @return list of RollingStock ordered by RollingStock location 342 */ 343 public List<T> getByLocationList() { 344 return getByList(getByNumberList(), BY_LOCATION); 345 } 346 347 /** 348 * Sort by rolling stock destination 349 * 350 * @return list of RollingStock ordered by RollingStock destination 351 */ 352 public List<T> getByDestinationList() { 353 return getByList(getByLocationList(), BY_DESTINATION); 354 } 355 356 /** 357 * Sort by rolling stocks in trains 358 * 359 * @return list of RollingStock ordered by trains 360 */ 361 public List<T> getByTrainList() { 362 List<T> byDest = getByList(getByIdList(), BY_DESTINATION); 363 List<T> byLoc = getByList(byDest, BY_LOCATION); 364 return getByList(byLoc, BY_TRAIN); 365 } 366 367 /** 368 * Sort by rolling stock moves 369 * 370 * @return list of RollingStock ordered by RollingStock moves 371 */ 372 public List<T> getByMovesList() { 373 return getByList(getList(), BY_MOVES); 374 } 375 376 /** 377 * Sort by when rolling stock was built 378 * 379 * @return list of RollingStock ordered by RollingStock built date 380 */ 381 public List<T> getByBuiltList() { 382 return getByList(getByIdList(), BY_BUILT); 383 } 384 385 /** 386 * Sort by rolling stock owner 387 * 388 * @return list of RollingStock ordered by RollingStock owner 389 */ 390 public List<T> getByOwnerList() { 391 return getByList(getByIdList(), BY_OWNER); 392 } 393 394 /** 395 * Sort by rolling stock value 396 * 397 * @return list of RollingStock ordered by value 398 */ 399 public List<T> getByValueList() { 400 return getByList(getByIdList(), BY_VALUE); 401 } 402 403 /** 404 * Sort by rolling stock RFID 405 * 406 * @return list of RollingStock ordered by RFIDs 407 */ 408 public List<T> getByRfidList() { 409 return getByList(getByIdList(), BY_RFID); 410 } 411 412 public List<T> getByPickupList() { 413 return getByList(getByDestinationList(), BY_PICKUP); 414 } 415 416 /** 417 * Get a list of all rolling stock sorted last date used 418 * 419 * @return list of RollingStock ordered by last date 420 */ 421 public List<T> getByLastDateList() { 422 return getByList(getByIdList(), BY_LAST); 423 } 424 425 public List<T> getByLastDateReversedList() { 426 List<T> out = getByLastDateList(); 427 Collections.reverse(out); 428 return out; 429 } 430 431 public List<T> getByCommentList() { 432 return getByList(getByIdList(), BY_COMMENT); 433 } 434 435 protected List<T> getByList(List<T> sortIn, int attribute) { 436 List<T> out = new ArrayList<>(sortIn); 437 out.sort(getComparator(attribute)); 438 return out; 439 } 440 441 // The various sort options for RollingStock 442 // see CarManager and EngineManger for other values 443 protected static final int BY_NUMBER = 0; 444 protected static final int BY_ROAD = 1; 445 protected static final int BY_TYPE = 2; 446 protected static final int BY_COLOR = 3; 447 protected static final int BY_LOCATION = 4; 448 protected static final int BY_DESTINATION = 5; 449 protected static final int BY_TRAIN = 6; 450 protected static final int BY_MOVES = 7; 451 protected static final int BY_BUILT = 8; 452 protected static final int BY_OWNER = 9; 453 protected static final int BY_RFID = 10; 454 protected static final int BY_VALUE = 11; 455 protected static final int BY_LAST = 12; 456 protected static final int BY_BLOCKING = 13; 457 private static final int BY_PICKUP = 14; 458 protected static final int BY_COMMENT = 15; 459 460 protected java.util.Comparator<T> getComparator(int attribute) { 461 switch (attribute) { 462 case BY_NUMBER: 463 return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber())); 464 case BY_ROAD: 465 return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName())); 466 case BY_TYPE: 467 return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName())); 468 case BY_COLOR: 469 return (r1, r2) -> (r1.getColorName().compareToIgnoreCase(r2.getColorName())); 470 case BY_LOCATION: 471 return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName()) 472 .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName()); 473 case BY_DESTINATION: 474 return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName()) 475 .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName()); 476 case BY_TRAIN: 477 return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName())); 478 case BY_MOVES: 479 return (r1, r2) -> (r1.getMoves() - r2.getMoves()); 480 case BY_BUILT: 481 return (r1, 482 r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt()))); 483 case BY_OWNER: 484 return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName())); 485 case BY_RFID: 486 return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid())); 487 case BY_VALUE: 488 return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue())); 489 case BY_LAST: 490 return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate())); 491 case BY_BLOCKING: 492 return (r1, r2) -> (r1.getBlocking() - r2.getBlocking()); 493 case BY_PICKUP: 494 return (r1, r2) -> (r1.getPickupTime().compareToIgnoreCase(r2.getPickupTime())); 495 case BY_COMMENT: 496 return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment())); 497 default: 498 return (r1, r2) -> ((r1.getRoadName() + r1.getNumber()) 499 .compareToIgnoreCase(r2.getRoadName() + r2.getNumber())); 500 } 501 } 502 503 protected List<T> sortByTrackPriority(List<T> list) { 504 List<T> out = new ArrayList<>(); 505 // sort rolling stock by track priority 506 for (T rs : list) { 507 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) { 508 out.add(rs); 509 } 510 } 511 for (T rs : list) { 512 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) { 513 out.add(rs); 514 } 515 } 516 for (T rs : list) { 517 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) { 518 out.add(rs); 519 } 520 } 521 for (T rs : list) { 522 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) { 523 out.add(rs); 524 } 525 } 526 // rolling stock without a track assignment 527 for (T rs : list) { 528 if (!out.contains(rs)) { 529 out.add(rs); 530 } 531 } 532 return out; 533 } 534 535 /* 536 * Converts build date into consistent String. Three build date formats; Two 537 * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY. 538 */ 539 public static String convertBuildDate(String date) { 540 String[] built = date.split("-"); 541 if (built.length == 2) { 542 try { 543 int d = Integer.parseInt(built[1]); 544 if (d < 100) { 545 d = d + 1900; 546 } 547 return Integer.toString(d); 548 } catch (NumberFormatException e) { 549 log.debug("Unable to parse built date ({})", date); 550 } 551 } else { 552 try { 553 int d = Integer.parseInt(date); 554 if (d < 100) { 555 d = d + 1900; 556 } 557 return Integer.toString(d); 558 } catch (NumberFormatException e) { 559 log.debug("Unable to parse built date ({})", date); 560 } 561 } 562 return date; 563 } 564 565 /** 566 * Get a list of rolling stocks assigned to a train ordered by location 567 * 568 * @param train The Train. 569 * 570 * @return List of RollingStock assigned to the train ordered by location 571 */ 572 public List<T> getByTrainList(Train train) { 573 return getByList(getList(train), BY_LOCATION); 574 } 575 576 /** 577 * Returns a list (no order) of RollingStock in a train. 578 * 579 * @param train The Train. 580 * 581 * @return list of RollingStock 582 */ 583 public List<T> getList(Train train) { 584 List<T> out = new ArrayList<>(); 585 getList().stream().filter((rs) -> { 586 return rs.getTrain() == train; 587 }).forEachOrdered((rs) -> { 588 out.add(rs); 589 }); 590 return out; 591 } 592 593 /** 594 * Returns a list (no order) of RollingStock at a location. 595 * 596 * @param location location to search for. 597 * @return list of RollingStock 598 */ 599 public List<T> getList(Location location) { 600 List<T> out = new ArrayList<>(); 601 getList().stream().filter((rs) -> { 602 return rs.getLocation() == location; 603 }).forEachOrdered((rs) -> { 604 out.add(rs); 605 }); 606 return out; 607 } 608 609 /** 610 * Returns a list (no order) of RollingStock on a track. 611 * 612 * @param track Track to search for. 613 * @return list of RollingStock 614 */ 615 public List<T> getList(Track track) { 616 List<T> out = new ArrayList<>(); 617 getList().stream().filter((rs) -> { 618 return rs.getTrack() == track; 619 }).forEachOrdered((rs) -> { 620 out.add(rs); 621 }); 622 return out; 623 } 624 625 /** 626 * Returns the rolling stock's last clone if there's one. 627 * 628 * @param rs The rolling stock searching for a clone 629 * @return Returns the rolling stock's last clone, null if there isn't a 630 * clone. 631 */ 632 public T getClone(RollingStock rs) { 633 List<T> list = getByLastDateReversedList(); 634 // clone with the highest creation number will be first in this list 635 for (T clone : list) { 636 if (clone.isClone() && 637 clone.getDestinationTrack() == rs.getTrack() && 638 clone.getRoadName().equals(rs.getRoadName()) && 639 clone.getNumber().split(RollingStock.CLONE_REGEX)[0].equals(rs.getNumber())) { 640 return clone; 641 } 642 } 643 return null; // no clone for this rolling stock 644 } 645 646 int cloneCreationOrder = 0; 647 648 /** 649 * Returns the highest clone creation order given to a clone. 650 * 651 * @return 1 if the first clone created, otherwise the highest found plus 652 * one. Automatically increments. 653 */ 654 private int getCloneCreationOrder() { 655 if (cloneCreationOrder == 0) { 656 for (RollingStock rs : getList()) { 657 if (rs.isClone()) { 658 String[] number = rs.getNumber().split(RollingStock.CLONE_REGEX); 659 int creationOrder = Integer.parseInt(number[1]); 660 if (creationOrder > cloneCreationOrder) { 661 cloneCreationOrder = creationOrder; 662 } 663 } 664 } 665 } 666 return ++cloneCreationOrder; 667 } 668 669 /** 670 * Creates a clone of rolling stock and places it at the rolling stocks location and track. 671 * @param rs the rolling stock requesting a clone 672 * @return the clone of the rolling stock 673 */ 674 protected T createClone(RollingStock rs) { 675 int cloneCreationOrder = getCloneCreationOrder(); 676 return createClone(rs, cloneCreationOrder); 677 } 678 679 protected T createClone(RollingStock rs, int cloneCreationOrder) { 680 @SuppressWarnings("unchecked") 681 T clone = (T) rs.copy(); 682 clone.setNumber(rs.getNumber() + RollingStock.CLONE + padNumber(cloneCreationOrder)); 683 clone.setClone(true); 684 clone.setMoves(rs.getMoves()); 685 // register car before setting location so the car gets logged 686 register(clone); 687 clone.setLocation(rs.getLocation(), rs.getTrack(), RollingStock.FORCE); 688 clone.addPropertyChangeListeners(); 689 rs.setCloneOrder(cloneCreationOrder); // for reset 690 return clone; 691 } 692 693 /** 694 * Moves the rolling stock to the clone's destination track 695 * @param rs rolling stock to be moved 696 * @param track the destination track for the clone 697 * @param train the train that will transport the clone 698 * @param startTime when the rolling stock was moved 699 * @param clone the clone being transported by the train 700 */ 701 protected void finshCreateClone(RollingStock rs, Track track, Train train, LocalDateTime startTime, RollingStock clone) { 702 rs.setMoves(rs.getMoves() + 1); // bump count 703 rs.setLocation(track.getLocation(), track, RollingStock.FORCE); 704 rs.setLastTrain(train); 705 rs.setLastLocationId(clone.getLocationId()); 706 rs.setLastTrackId(clone.getTrackId()); 707 rs.setLastRouteId(train.getRoute().getId()); 708 // this rs was moved during the build process 709 rs.setLastDate(startTime); 710 rs.setDestination(null, null); 711 } 712 713 /** 714 * Pads the number to 4 digits for sorting purposes 715 * 716 * @param n the number needed leading zeros 717 * @return String "number" with leading zeros if necessary 718 */ 719 protected String padNumber(int n) { 720 return String.format("%04d", n); 721 } 722 723 @Override 724 @OverridingMethodsMustInvokeSuper 725 public void propertyChange(PropertyChangeEvent evt) { 726 if (evt.getPropertyName().equals(Xml.ID)) { 727 @SuppressWarnings("unchecked") 728 T rs = (T) evt.getSource(); // unchecked cast to T 729 _hashTable.remove(evt.getOldValue()); 730 if (_hashTable.containsKey(rs.getId())) { 731 log.error("Duplicate rolling stock id: ({})", rs.getId()); 732 rs.dispose(); 733 } else { 734 _hashTable.put(rs.getId(), rs); 735 } 736 // fire so listeners that rebuild internal lists get signal of change in id, even without change in size 737 firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size()); 738 } 739 } 740 741 private static final Logger log = LoggerFactory.getLogger(RollingStockManager.class); 742 743}