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> getByCommentList() { 425 return getByList(getByIdList(), BY_COMMENT); 426 } 427 428 /** 429 * Sort a specific list of rolling stock last date used 430 * 431 * @param inList list of rolling stock to sort. 432 * @return list of RollingStock ordered by last date 433 */ 434 public List<T> getByLastDateList(List<T> inList) { 435 return getByList(inList, BY_LAST); 436 } 437 438 protected List<T> getByList(List<T> sortIn, int attribute) { 439 List<T> out = new ArrayList<>(sortIn); 440 out.sort(getComparator(attribute)); 441 return out; 442 } 443 444 // The various sort options for RollingStock 445 // see CarManager and EngineManger for other values 446 protected static final int BY_NUMBER = 0; 447 protected static final int BY_ROAD = 1; 448 protected static final int BY_TYPE = 2; 449 protected static final int BY_COLOR = 3; 450 protected static final int BY_LOCATION = 4; 451 protected static final int BY_DESTINATION = 5; 452 protected static final int BY_TRAIN = 6; 453 protected static final int BY_MOVES = 7; 454 protected static final int BY_BUILT = 8; 455 protected static final int BY_OWNER = 9; 456 protected static final int BY_RFID = 10; 457 protected static final int BY_VALUE = 11; 458 protected static final int BY_LAST = 12; 459 protected static final int BY_BLOCKING = 13; 460 private static final int BY_PICKUP = 14; 461 protected static final int BY_COMMENT = 15; 462 463 protected java.util.Comparator<T> getComparator(int attribute) { 464 switch (attribute) { 465 case BY_NUMBER: 466 return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber())); 467 case BY_ROAD: 468 return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName())); 469 case BY_TYPE: 470 return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName())); 471 case BY_COLOR: 472 return (r1, r2) -> (r1.getColor().compareToIgnoreCase(r2.getColor())); 473 case BY_LOCATION: 474 return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName()) 475 .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName()); 476 case BY_DESTINATION: 477 return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName()) 478 .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName()); 479 case BY_TRAIN: 480 return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName())); 481 case BY_MOVES: 482 return (r1, r2) -> (r1.getMoves() - r2.getMoves()); 483 case BY_BUILT: 484 return (r1, 485 r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt()))); 486 case BY_OWNER: 487 return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName())); 488 case BY_RFID: 489 return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid())); 490 case BY_VALUE: 491 return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue())); 492 case BY_LAST: 493 return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate())); 494 case BY_BLOCKING: 495 return (r1, r2) -> (r1.getBlocking() - r2.getBlocking()); 496 case BY_PICKUP: 497 return (r1, r2) -> (r1.getPickupTime().compareToIgnoreCase(r2.getPickupTime())); 498 case BY_COMMENT: 499 return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment())); 500 default: 501 return (r1, r2) -> ((r1.getRoadName() + r1.getNumber()) 502 .compareToIgnoreCase(r2.getRoadName() + r2.getNumber())); 503 } 504 } 505 506 protected List<T> sortByTrackPriority(List<T> list) { 507 List<T> out = new ArrayList<>(); 508 // sort rolling stock by track priority 509 for (T rs : list) { 510 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) { 511 out.add(rs); 512 } 513 } 514 for (T rs : list) { 515 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) { 516 out.add(rs); 517 } 518 } 519 for (T rs : list) { 520 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) { 521 out.add(rs); 522 } 523 } 524 for (T rs : list) { 525 if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) { 526 out.add(rs); 527 } 528 } 529 // rolling stock without a track assignment 530 for (T rs : list) { 531 if (!out.contains(rs)) { 532 out.add(rs); 533 } 534 } 535 return out; 536 } 537 538 /* 539 * Converts build date into consistent String. Three build date formats; Two 540 * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY. 541 */ 542 public static String convertBuildDate(String date) { 543 String[] built = date.split("-"); 544 if (built.length == 2) { 545 try { 546 int d = Integer.parseInt(built[1]); 547 if (d < 100) { 548 d = d + 1900; 549 } 550 return Integer.toString(d); 551 } catch (NumberFormatException e) { 552 log.debug("Unable to parse built date ({})", date); 553 } 554 } else { 555 try { 556 int d = Integer.parseInt(date); 557 if (d < 100) { 558 d = d + 1900; 559 } 560 return Integer.toString(d); 561 } catch (NumberFormatException e) { 562 log.debug("Unable to parse built date ({})", date); 563 } 564 } 565 return date; 566 } 567 568 /** 569 * Get a list of rolling stocks assigned to a train ordered by location 570 * 571 * @param train The Train. 572 * 573 * @return List of RollingStock assigned to the train ordered by location 574 */ 575 public List<T> getByTrainList(Train train) { 576 return getByList(getList(train), BY_LOCATION); 577 } 578 579 /** 580 * Returns a list (no order) of RollingStock in a train. 581 * 582 * @param train The Train. 583 * 584 * @return list of RollingStock 585 */ 586 public List<T> getList(Train train) { 587 List<T> out = new ArrayList<>(); 588 getList().stream().filter((rs) -> { 589 return rs.getTrain() == train; 590 }).forEachOrdered((rs) -> { 591 out.add(rs); 592 }); 593 return out; 594 } 595 596 /** 597 * Returns a list (no order) of RollingStock at a location. 598 * 599 * @param location location to search for. 600 * @return list of RollingStock 601 */ 602 public List<T> getList(Location location) { 603 List<T> out = new ArrayList<>(); 604 getList().stream().filter((rs) -> { 605 return rs.getLocation() == location; 606 }).forEachOrdered((rs) -> { 607 out.add(rs); 608 }); 609 return out; 610 } 611 612 /** 613 * Returns a list (no order) of RollingStock on a track. 614 * 615 * @param track Track to search for. 616 * @return list of RollingStock 617 */ 618 public List<T> getList(Track track) { 619 List<T> out = new ArrayList<>(); 620 getList().stream().filter((rs) -> { 621 return rs.getTrack() == track; 622 }).forEachOrdered((rs) -> { 623 out.add(rs); 624 }); 625 return out; 626 } 627 628 /** 629 * Returns the rolling stock's last clone rolling stock if there's one. 630 * 631 * @param rs The rolling stock searching for a clone 632 * @return Returns the rolling stock's last clone rolling stock, null if 633 * there isn't a clone rolling stock. 634 */ 635 public T getClone(RollingStock rs) { 636 List<T> list = getByLastDateList(); 637 // clone with the highest creation number will be last in the list 638 for (int i = list.size() - 1; i >= 0; i--) { 639 T kar = list.get(i); 640 if (kar.isClone() && 641 kar.getDestinationTrack() == rs.getTrack() && 642 kar.getRoadName().equals(rs.getRoadName()) && 643 kar.getNumber().split(RollingStock.CLONE_REGEX)[0].equals(rs.getNumber())) { 644 return kar; 645 } 646 } 647 return null; // no clone for this rolling stock 648 } 649 650 int cloneCreationOrder = 0; 651 652 /** 653 * Returns the highest clone creation order given to a clone. 654 * 655 * @return 1 if the first clone created, otherwise the highest found plus 656 * one. Automatically increments. 657 */ 658 protected int getCloneCreationOrder() { 659 if (cloneCreationOrder == 0) { 660 for (RollingStock rs : getList()) { 661 if (rs.isClone()) { 662 String[] number = rs.getNumber().split(RollingStock.CLONE_REGEX); 663 int creationOrder = Integer.parseInt(number[1]); 664 if (creationOrder > cloneCreationOrder) { 665 cloneCreationOrder = creationOrder; 666 } 667 } 668 } 669 } 670 return ++cloneCreationOrder; 671 } 672 673 /** 674 * Adds 4 leading zeros to the number for sorting purposes 675 * 676 * @param n the number needed leading zeros 677 * @return String "number" with 4 leading zeros 678 */ 679 protected String padNumber(int n) { 680 return String.format("%04d", n); 681 } 682 683 @Override 684 @OverridingMethodsMustInvokeSuper 685 public void propertyChange(PropertyChangeEvent evt) { 686 if (evt.getPropertyName().equals(Xml.ID)) { 687 @SuppressWarnings("unchecked") 688 T rs = (T) evt.getSource(); // unchecked cast to T 689 _hashTable.remove(evt.getOldValue()); 690 if (_hashTable.containsKey(rs.getId())) { 691 log.error("Duplicate rolling stock id: ({})", rs.getId()); 692 rs.dispose(); 693 } else { 694 _hashTable.put(rs.getId(), rs); 695 } 696 // fire so listeners that rebuild internal lists get signal of change in id, even without change in size 697 firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size()); 698 } 699 } 700 701 private final static Logger log = LoggerFactory.getLogger(RollingStockManager.class); 702 703}