001package jmri.jmrit.operations.locations.schedules; 002 003import java.util.*; 004 005import org.jdom2.Element; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import jmri.InstanceManager; 010import jmri.beans.PropertyChangeSupport; 011import jmri.jmrit.operations.locations.*; 012import jmri.jmrit.operations.rollingstock.cars.*; 013import jmri.jmrit.operations.setup.Control; 014import jmri.jmrit.operations.trains.schedules.TrainSchedule; 015import jmri.jmrit.operations.trains.schedules.TrainScheduleManager; 016 017/** 018 * Represents a car delivery schedule for a location 019 * 020 * @author Daniel Boudreau Copyright (C) 2009, 2011, 2013 021 */ 022public class Schedule extends PropertyChangeSupport implements java.beans.PropertyChangeListener { 023 024 protected String _id = ""; 025 protected String _name = ""; 026 protected String _comment = ""; 027 028 // stores ScheduleItems for this schedule 029 protected Hashtable<String, ScheduleItem> _scheduleHashTable = new Hashtable<String, ScheduleItem>(); 030 protected int _IdNumber = 0; // each item in a schedule gets its own id 031 protected int _sequenceNum = 0; // each item has a unique sequence number 032 033 public static final String LISTCHANGE_CHANGED_PROPERTY = "scheduleListChange"; // NOI18N 034 public static final String DISPOSE = "scheduleDispose"; // NOI18N 035 036 public static final String SCHEDULE_OKAY = ""; // NOI18N 037 038 public Schedule(String id, String name) { 039 log.debug("New schedule ({}) id: {}", name, id); 040 _name = name; 041 _id = id; 042 } 043 044 public String getId() { 045 return _id; 046 } 047 048 public void setName(String name) { 049 String old = _name; 050 _name = name; 051 if (!old.equals(name)) { 052 setDirtyAndFirePropertyChange("ScheduleName", old, name); // NOI18N 053 } 054 } 055 056 // for combo boxes 057 @Override 058 public String toString() { 059 return _name; 060 } 061 062 public String getName() { 063 return _name; 064 } 065 066 public int getSize() { 067 return _scheduleHashTable.size(); 068 } 069 070 public void setComment(String comment) { 071 String old = _comment; 072 _comment = comment; 073 if (!old.equals(comment)) { 074 setDirtyAndFirePropertyChange("ScheduleComment", old, comment); // NOI18N 075 } 076 } 077 078 public String getComment() { 079 return _comment; 080 } 081 082 public void dispose() { 083 setDirtyAndFirePropertyChange(DISPOSE, null, DISPOSE); 084 } 085 086 public void resetHitCounts() { 087 for (ScheduleItem si : getItemsByIdList()) { 088 si.setHits(0); 089 } 090 } 091 092 /** 093 * Adds a car type to the end of this schedule 094 * 095 * @param type The string car type to add. 096 * @return ScheduleItem created for the car type added 097 */ 098 public ScheduleItem addItem(String type) { 099 _IdNumber++; 100 _sequenceNum++; 101 String id = _id + "c" + Integer.toString(_IdNumber); 102 log.debug("Adding new item to ({}) id: {}", getName(), id); 103 ScheduleItem si = new ScheduleItem(id, type); 104 si.setSequenceId(_sequenceNum); 105 Integer old = Integer.valueOf(_scheduleHashTable.size()); 106 _scheduleHashTable.put(si.getId(), si); 107 108 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size())); 109 // listen for set out and pick up changes to forward 110 si.addPropertyChangeListener(this); 111 return si; 112 } 113 114 /** 115 * Add a schedule item at a specific place (sequence) in the schedule 116 * Allowable sequence numbers are 0 to max size of schedule. 0 = start of 117 * list. 118 * 119 * @param carType The string car type name to add. 120 * @param sequence Where in the schedule to add the item. 121 * @return schedule item 122 */ 123 public ScheduleItem addItem(String carType, int sequence) { 124 ScheduleItem si = addItem(carType); 125 if (sequence < 0 || sequence > _scheduleHashTable.size()) { 126 return si; 127 } 128 for (int i = 0; i < _scheduleHashTable.size() - sequence - 1; i++) { 129 moveItemUp(si); 130 } 131 return si; 132 } 133 134 /** 135 * Remember a NamedBean Object created outside the manager. 136 * 137 * @param si The schedule item to add. 138 */ 139 public void register(ScheduleItem si) { 140 Integer old = Integer.valueOf(_scheduleHashTable.size()); 141 _scheduleHashTable.put(si.getId(), si); 142 143 // find last id created 144 String[] getId = si.getId().split("c"); 145 int id = Integer.parseInt(getId[1]); 146 if (id > _IdNumber) { 147 _IdNumber = id; 148 } 149 // find highest sequence number 150 if (si.getSequenceId() > _sequenceNum) { 151 _sequenceNum = si.getSequenceId(); 152 } 153 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size())); 154 // listen for set out and pick up changes to forward 155 si.addPropertyChangeListener(this); 156 } 157 158 /** 159 * Delete a ScheduleItem 160 * 161 * @param si The scheduleItem to delete. 162 */ 163 public void deleteItem(ScheduleItem si) { 164 if (si != null) { 165 si.removePropertyChangeListener(this); 166 // subtract from the items's available track length 167 String id = si.getId(); 168 si.dispose(); 169 Integer old = Integer.valueOf(_scheduleHashTable.size()); 170 _scheduleHashTable.remove(id); 171 resequenceIds(); 172 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size())); 173 } 174 } 175 176 /** 177 * Reorder the item sequence numbers for this schedule 178 */ 179 private void resequenceIds() { 180 List<ScheduleItem> scheduleItems = getItemsBySequenceList(); 181 for (int i = 0; i < scheduleItems.size(); i++) { 182 scheduleItems.get(i).setSequenceId(i + 1); // start sequence numbers 183 // at 1 184 _sequenceNum = i + 1; 185 } 186 } 187 188 /** 189 * Get item by car type (gets last schedule item with this type) 190 * 191 * @param carType The string car type to search for. 192 * @return schedule item 193 */ 194 public ScheduleItem getItemByType(String carType) { 195 List<ScheduleItem> scheduleSequenceList = getItemsBySequenceList(); 196 ScheduleItem si; 197 198 for (int i = scheduleSequenceList.size() - 1; i >= 0; i--) { 199 si = scheduleSequenceList.get(i); 200 if (si.getTypeName().equals(carType)) { 201 return si; 202 } 203 } 204 return null; 205 } 206 207 /** 208 * Get a ScheduleItem by id 209 * 210 * @param id The string id of the ScheduleItem. 211 * @return schedule item 212 */ 213 public ScheduleItem getItemById(String id) { 214 return _scheduleHashTable.get(id); 215 } 216 217 private List<ScheduleItem> getItemsByIdList() { 218 String[] arr = new String[_scheduleHashTable.size()]; 219 List<ScheduleItem> out = new ArrayList<ScheduleItem>(); 220 Enumeration<String> en = _scheduleHashTable.keys(); 221 int i = 0; 222 while (en.hasMoreElements()) { 223 arr[i++] = en.nextElement(); 224 } 225 Arrays.sort(arr); 226 for (i = 0; i < arr.length; i++) { 227 out.add(getItemById(arr[i])); 228 } 229 return out; 230 } 231 232 /** 233 * Get a list of ScheduleItems sorted by schedule order 234 * 235 * @return list of ScheduleItems ordered by sequence 236 */ 237 public List<ScheduleItem> getItemsBySequenceList() { 238 // first get id list 239 List<ScheduleItem> sortList = getItemsByIdList(); 240 // now re-sort 241 List<ScheduleItem> out = new ArrayList<ScheduleItem>(); 242 243 for (ScheduleItem si : sortList) { 244 for (int j = 0; j < out.size(); j++) { 245 if (si.getSequenceId() < out.get(j).getSequenceId()) { 246 out.add(j, si); 247 break; 248 } 249 } 250 if (!out.contains(si)) { 251 out.add(si); 252 } 253 } 254 return out; 255 } 256 257 /** 258 * Places a ScheduleItem earlier in the schedule 259 * 260 * @param si The ScheduleItem to move. 261 */ 262 public void moveItemUp(ScheduleItem si) { 263 int sequenceId = si.getSequenceId(); 264 if (sequenceId - 1 <= 0) { 265 si.setSequenceId(_sequenceNum + 1); // move to the end of the list 266 resequenceIds(); 267 } else { 268 // adjust the other item taken by this one 269 ScheduleItem replaceSi = getItemBySequenceId(sequenceId - 1); 270 if (replaceSi != null) { 271 replaceSi.setSequenceId(sequenceId); 272 si.setSequenceId(sequenceId - 1); 273 } else { 274 resequenceIds(); // error the sequence number is missing 275 } 276 } 277 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceId)); 278 } 279 280 /** 281 * Places a ScheduleItem later in the schedule 282 * 283 * @param si The ScheduleItem to move. 284 */ 285 public void moveItemDown(ScheduleItem si) { 286 int sequenceId = si.getSequenceId(); 287 if (sequenceId + 1 > _sequenceNum) { 288 si.setSequenceId(0); // move to the start of the list 289 resequenceIds(); 290 } else { 291 // adjust the other item taken by this one 292 ScheduleItem replaceSi = getItemBySequenceId(sequenceId + 1); 293 if (replaceSi != null) { 294 replaceSi.setSequenceId(sequenceId); 295 si.setSequenceId(sequenceId + 1); 296 } else { 297 resequenceIds(); // error the sequence number is missing 298 } 299 } 300 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceId)); 301 } 302 303 public ScheduleItem getItemBySequenceId(int sequenceId) { 304 for (ScheduleItem si : getItemsByIdList()) { 305 if (si.getSequenceId() == sequenceId) { 306 return si; 307 } 308 } 309 return null; 310 } 311 312 /** 313 * Check to see if schedule is valid for the track. 314 * 315 * @param track The track associated with this schedule 316 * @return SCHEDULE_OKAY if schedule okay, otherwise an error message. 317 */ 318 public String checkScheduleValid(Track track) { 319 String status = SCHEDULE_OKAY; 320 List<ScheduleItem> scheduleItems = getItemsBySequenceList(); 321 if (scheduleItems.size() == 0) { 322 return Bundle.getMessage("empty"); 323 } 324 for (ScheduleItem si : scheduleItems) { 325 // check train schedules 326 if (!si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) && 327 InstanceManager.getDefault(TrainScheduleManager.class) 328 .getScheduleById(si.getSetoutTrainScheduleId()) == null) { 329 status = Bundle.getMessage("NotValid", si.getSetoutTrainScheduleId()); 330 break; 331 } 332 if (!si.getPickupTrainScheduleId().equals(ScheduleItem.NONE) && 333 InstanceManager.getDefault(TrainScheduleManager.class) 334 .getScheduleById(si.getPickupTrainScheduleId()) == null) { 335 status = Bundle.getMessage("NotValid", si.getPickupTrainScheduleId()); 336 break; 337 } 338 if (!track.getLocation().acceptsTypeName(si.getTypeName())) { 339 status = Bundle.getMessage("NotValid", si.getTypeName()); 340 break; 341 } 342 if (!track.isTypeNameAccepted(si.getTypeName())) { 343 status = Bundle.getMessage("NotValid", si.getTypeName()); 344 break; 345 } 346 // check roads, accepted by track, valid road, and there's at least 347 // one car with 348 // that road 349 if (!si.getRoadName().equals(ScheduleItem.NONE) && 350 (!track.isRoadNameAccepted(si.getRoadName()) || 351 !InstanceManager.getDefault(CarRoads.class).containsName(si.getRoadName()) || 352 InstanceManager.getDefault(CarManager.class).getByTypeAndRoad(si.getTypeName(), 353 si.getRoadName()) == null)) { 354 status = Bundle.getMessage("NotValid", si.getRoadName()); 355 break; 356 } 357 // check loads 358 List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName()); 359 if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && 360 (!track.isLoadNameAndCarTypeAccepted(si.getReceiveLoadName(), si.getTypeName()) || 361 !loads.contains(si.getReceiveLoadName()))) { 362 status = Bundle.getMessage("NotValid", si.getReceiveLoadName()); 363 break; 364 } 365 if (!si.getShipLoadName().equals(ScheduleItem.NONE) && !loads.contains(si.getShipLoadName())) { 366 status = Bundle.getMessage("NotValid", si.getShipLoadName()); 367 break; 368 } 369 // check destination 370 if (si.getDestination() != null && 371 (!si.getDestination().acceptsTypeName(si.getTypeName()) || 372 InstanceManager.getDefault(LocationManager.class) 373 .getLocationById(si.getDestination().getId()) == null)) { 374 status = Bundle.getMessage("NotValid", si.getDestination()); 375 break; 376 } 377 // check destination track 378 if (si.getDestination() != null && si.getDestinationTrack() != null) { 379 if (!si.getDestination().isTrackAtLocation(si.getDestinationTrack())) { 380 status = Bundle.getMessage("NotValid", 381 si.getDestinationTrack() + " (" + Bundle.getMessage("Track") + ")"); 382 break; 383 } 384 if (!si.getDestinationTrack().isTypeNameAccepted(si.getTypeName())) { 385 status = Bundle.getMessage("NotValid", 386 si.getDestinationTrack() + " (" + Bundle.getMessage("Type") + ")"); 387 break; 388 } 389 if (!si.getRoadName().equals(ScheduleItem.NONE) && 390 !si.getDestinationTrack().isRoadNameAccepted(si.getRoadName())) { 391 status = Bundle.getMessage("NotValid", 392 si.getDestinationTrack() + " (" + Bundle.getMessage("Road") + ")"); 393 break; 394 } 395 if (!si.getShipLoadName().equals(ScheduleItem.NONE) && 396 !si.getDestinationTrack().isLoadNameAndCarTypeAccepted(si.getShipLoadName(), 397 si.getTypeName())) { 398 status = Bundle.getMessage("NotValid", 399 si.getDestinationTrack() + " (" + Bundle.getMessage("Load") + ")"); 400 break; 401 } 402 } 403 } 404 return status; 405 } 406 407 private static boolean debugFlag = false; 408 409 /* 410 * Match mode search 411 */ 412 public String searchSchedule(Car car, Track track) { 413 if (debugFlag) { 414 log.debug("Search match for car ({}) type ({}) load ({})", car.toString(), car.getTypeName(), 415 car.getLoadName()); 416 } 417 // has the car already been assigned a schedule item? Then verify that 418 // its still okay 419 if (!car.getScheduleItemId().equals(Track.NONE)) { 420 ScheduleItem si = getItemById(car.getScheduleItemId()); 421 if (si != null) { 422 String status = checkScheduleItem(si, car, track); 423 if (status.equals(Track.OKAY)) { 424 track.setScheduleItemId(si.getId()); 425 return Track.OKAY; 426 } 427 log.debug("Car ({}) with schedule id ({}) failed check, status: {}", car.toString(), 428 car.getScheduleItemId(), status); 429 } 430 } 431 // first check to see if the schedule services car type 432 if (!checkScheduleAttribute(Track.TYPE, car.getTypeName(), car)) { 433 return Track.SCHEDULE + " " + Bundle.getMessage("scheduleNotType", getName(), car.getTypeName()); 434 } 435 436 // search schedule for a match 437 for (int i = 0; i < getSize(); i++) { 438 ScheduleItem si = track.getNextScheduleItem(); 439 if (debugFlag) { 440 log.debug("Item id: ({}) requesting type ({}) load ({}) final dest ({}, {})", si.getId(), 441 si.getTypeName(), si.getReceiveLoadName(), si.getDestinationName(), 442 si.getDestinationTrackName()); // NOI18N 443 } 444 String status = checkScheduleItem(si, car, track); 445 if (status.equals(Track.OKAY)) { 446 log.debug("Found item match ({}) car ({}) type ({}) load ({}) ship ({}) destination ({}, {})", 447 si.getId(), car.toString(), car.getTypeName(), si.getReceiveLoadName(), si.getShipLoadName(), 448 si.getDestinationName(), si.getDestinationTrackName()); // NOI18N 449 car.setScheduleItemId(si.getId()); // remember which item was a 450 // match 451 return Track.OKAY; 452 } else { 453 if (debugFlag) { 454 log.debug("Item id: ({}) status ({})", si.getId(), status); 455 } 456 } 457 } 458 if (debugFlag) { 459 log.debug("No Match"); 460 } 461 car.setScheduleItemId(Car.NONE); // clear the car's schedule id 462 return Track.SCHEDULE + " " + Bundle.getMessage("matchMessage", getName()); 463 } 464 465 public String checkScheduleItem(ScheduleItem si, Car car, Track track) { 466 // if car is already assigned to this schedule item allow it to be 467 // dropped off 468 // on the wrong day (car arrived late) 469 if (!car.getScheduleItemId().equals(si.getId()) && 470 !si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) && 471 !InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 472 .equals(si.getSetoutTrainScheduleId())) { 473 TrainSchedule trainSch = InstanceManager.getDefault(TrainScheduleManager.class) 474 .getScheduleById(si.getSetoutTrainScheduleId()); 475 if (trainSch != null) { 476 return Track.SCHEDULE + 477 " (" + 478 getName() + 479 ") " + 480 Bundle.getMessage("requestCarOnly") + 481 " (" + 482 trainSch.getName() + 483 ")"; 484 } 485 } 486 // Check for correct car type, road, load 487 if (!car.getTypeName().equals(si.getTypeName())) { 488 return Track.SCHEDULE + 489 " (" + 490 getName() + 491 ") " + 492 Bundle.getMessage("requestCar") + 493 " " + 494 Track.TYPE + 495 " (" + 496 si.getTypeName() + 497 ")"; 498 } 499 if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) { 500 return Track.SCHEDULE + 501 " (" + 502 getName() + 503 ") " + 504 Bundle.getMessage("requestCar") + 505 " " + 506 Track.TYPE + 507 " (" + 508 si.getTypeName() + 509 ") " + 510 Track.ROAD + 511 " (" + 512 si.getRoadName() + 513 ")"; 514 } 515 if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && !car.getLoadName().equals(si.getReceiveLoadName())) { 516 return Track.SCHEDULE + 517 " (" + 518 getName() + 519 ") " + 520 Bundle.getMessage("requestCar") + 521 " " + 522 Track.TYPE + 523 " (" + 524 si.getTypeName() + 525 ") " + 526 Track.LOAD + 527 " (" + 528 si.getReceiveLoadName() + 529 ")"; 530 } 531 // don't try the random feature if car is already assigned to this 532 // schedule item 533 if (car.getFinalDestinationTrack() != track && 534 !si.getRandom().equals(ScheduleItem.NONE) && 535 !car.getScheduleItemId().equals(si.getId())) { 536 if (!si.doRandom()) { 537 return Bundle.getMessage("scheduleRandom", Track.SCHEDULE, getName(), si.getId(), si.getRandom(), si.getCalculatedRandom()); 538 } 539 } 540 return Track.OKAY; 541 } 542 543 public boolean checkScheduleAttribute(String attribute, String carType, Car car) { 544 List<ScheduleItem> scheduleItems = getItemsBySequenceList(); 545 for (ScheduleItem si : scheduleItems) { 546 if (si.getTypeName().equals(carType)) { 547 // check to see if schedule services car type 548 if (attribute.equals(Track.TYPE)) { 549 return true; 550 } 551 // check to see if schedule services car type and load 552 if (attribute.equals(Track.LOAD) && 553 (si.getReceiveLoadName().equals(ScheduleItem.NONE) || 554 car == null || 555 si.getReceiveLoadName().equals(car.getLoadName()))) { 556 return true; 557 } 558 // check to see if schedule services car type and road 559 if (attribute.equals(Track.ROAD) && 560 (si.getRoadName().equals(ScheduleItem.NONE) || 561 car == null || 562 si.getRoadName().equals(car.getRoadName()))) { 563 return true; 564 } 565 // check to see if train schedule allows delivery 566 if (attribute.equals(Track.TRAIN_SCHEDULE) && 567 (si.getSetoutTrainScheduleId().isEmpty() || 568 InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 569 .equals(si.getSetoutTrainScheduleId()))) { 570 return true; 571 } 572 // check to see if at least one schedule item can service car 573 if (attribute.equals(Track.ALL) && 574 (si.getReceiveLoadName().equals(ScheduleItem.NONE) || 575 car == null || 576 si.getReceiveLoadName().equals(car.getLoadName())) && 577 (si.getRoadName().equals(ScheduleItem.NONE) || 578 car == null || 579 si.getRoadName().equals(car.getRoadName())) && 580 (si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) || 581 InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 582 .equals(si.getSetoutTrainScheduleId()))) { 583 return true; 584 } 585 } 586 } 587 return false; 588 } 589 590 /** 591 * Construct this Entry from XML. This member has to remain synchronized 592 * with the detailed DTD in operations-config.xml 593 * 594 * @param e Consist XML element 595 */ 596 public Schedule(Element e) { 597 org.jdom2.Attribute a; 598 if ((a = e.getAttribute(Xml.ID)) != null) { 599 _id = a.getValue(); 600 } else { 601 log.warn("no id attribute in schedule element when reading operations"); 602 } 603 if ((a = e.getAttribute(Xml.NAME)) != null) { 604 _name = a.getValue(); 605 } 606 if ((a = e.getAttribute(Xml.COMMENT)) != null) { 607 _comment = a.getValue(); 608 } 609 if (e.getChildren(Xml.ITEM) != null) { 610 List<Element> eScheduleItems = e.getChildren(Xml.ITEM); 611 log.debug("schedule: {} has {} items", getName(), eScheduleItems.size()); 612 for (Element eScheduleItem : eScheduleItems) { 613 register(new ScheduleItem(eScheduleItem)); 614 } 615 } 616 } 617 618 /** 619 * Create an XML element to represent this Entry. This member has to remain 620 * synchronized with the detailed DTD in operations-config.xml. 621 * 622 * @return Contents in a JDOM Element 623 */ 624 public org.jdom2.Element store() { 625 Element e = new org.jdom2.Element(Xml.SCHEDULE); 626 e.setAttribute(Xml.ID, getId()); 627 e.setAttribute(Xml.NAME, getName()); 628 e.setAttribute(Xml.COMMENT, getComment()); 629 for (ScheduleItem si : getItemsBySequenceList()) { 630 e.addContent(si.store()); 631 } 632 633 return e; 634 } 635 636 @Override 637 public void propertyChange(java.beans.PropertyChangeEvent e) { 638 if (Control.SHOW_PROPERTY) { 639 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e 640 .getNewValue()); 641 } 642 // forward all schedule item changes 643 setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue()); 644 } 645 646 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 647 // set dirty 648 InstanceManager.getDefault(LocationManagerXml.class).setDirty(true); 649 firePropertyChange(p, old, n); 650 } 651 652 private final static Logger log = LoggerFactory.getLogger(Schedule.class); 653 654}