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