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