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 } 350 else if (!si.getPickupTrainScheduleId().equals(ScheduleItem.NONE) && 351 InstanceManager.getDefault(TrainScheduleManager.class) 352 .getScheduleById(si.getPickupTrainScheduleId()) == null) { 353 status = Bundle.getMessage("NotValid", si.getPickupTrainScheduleId()); 354 } 355 else if (!track.getLocation().acceptsTypeName(si.getTypeName())) { 356 status = Bundle.getMessage("NotValid", si.getTypeName()); 357 } 358 else if (!track.isTypeNameAccepted(si.getTypeName())) { 359 status = Bundle.getMessage("NotValid", si.getTypeName()); 360 } 361 // check roads, accepted by track, valid road, and there's at least 362 // one car with that road 363 else if (!si.getRoadName().equals(ScheduleItem.NONE) && 364 (!track.isRoadNameAccepted(si.getRoadName()) || 365 !InstanceManager.getDefault(CarRoads.class).containsName(si.getRoadName()) || 366 InstanceManager.getDefault(CarManager.class).getByTypeAndRoad(si.getTypeName(), 367 si.getRoadName()) == null)) { 368 status = Bundle.getMessage("NotValid", si.getRoadName()); 369 } 370 // check loads 371 else if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && 372 (!track.isLoadNameAndCarTypeAccepted(si.getReceiveLoadName(), si.getTypeName()) || 373 !InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName()) 374 .contains(si.getReceiveLoadName()))) { 375 status = Bundle.getMessage("NotValid", si.getReceiveLoadName()); 376 } 377 else if (!si.getShipLoadName().equals(ScheduleItem.NONE) && 378 !InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName()).contains(si.getShipLoadName())) { 379 status = Bundle.getMessage("NotValid", si.getShipLoadName()); 380 } 381 // check destination 382 else if (si.getDestination() != null && 383 (!si.getDestination().acceptsTypeName(si.getTypeName()) || 384 InstanceManager.getDefault(LocationManager.class) 385 .getLocationById(si.getDestination().getId()) == null)) { 386 status = Bundle.getMessage("NotValid", si.getDestination()); 387 } 388 // check destination track 389 else if (si.getDestination() != null && si.getDestinationTrack() != null) { 390 if (!si.getDestination().isTrackAtLocation(si.getDestinationTrack())) { 391 status = Bundle.getMessage("NotValid", 392 si.getDestinationTrack() + " (" + Bundle.getMessage("Track") + ")"); 393 394 } 395 else if (!si.getDestinationTrack().isTypeNameAccepted(si.getTypeName())) { 396 status = Bundle.getMessage("NotValid", 397 si.getDestinationTrack() + " (" + Bundle.getMessage("Type") + ")"); 398 399 } 400 else if (!si.getRoadName().equals(ScheduleItem.NONE) && 401 !si.getDestinationTrack().isRoadNameAccepted(si.getRoadName())) { 402 status = Bundle.getMessage("NotValid", 403 si.getDestinationTrack() + " (" + Bundle.getMessage("Road") + ")"); 404 } 405 else if (!si.getShipLoadName().equals(ScheduleItem.NONE) && 406 !si.getDestinationTrack().isLoadNameAndCarTypeAccepted(si.getShipLoadName(), 407 si.getTypeName())) { 408 status = Bundle.getMessage("NotValid", 409 si.getDestinationTrack() + " (" + Bundle.getMessage("Load") + ")"); 410 } 411 } 412 return status; 413 } 414 415 private static boolean debugFlag = false; 416 417 /* 418 * Match mode search 419 */ 420 public String searchSchedule(Car car, Track track) { 421 if (debugFlag) { 422 log.debug("Search match for car ({}) type ({}) load ({})", car.toString(), car.getTypeName(), 423 car.getLoadName()); 424 } 425 // has the car already been assigned a schedule item? Then verify that 426 // its still okay 427 if (!car.getScheduleItemId().equals(Track.NONE)) { 428 ScheduleItem si = getItemById(car.getScheduleItemId()); 429 if (si != null) { 430 String status = checkScheduleItem(si, car, track); 431 if (status.equals(Track.OKAY)) { 432 track.setScheduleItemId(si.getId()); 433 return Track.OKAY; 434 } 435 log.debug("Car ({}) with schedule id ({}) failed check, status: {}", car.toString(), 436 car.getScheduleItemId(), status); 437 } 438 } 439 // first check to see if the schedule services car type 440 if (!checkScheduleAttribute(Track.TYPE, car.getTypeName(), car)) { 441 return Bundle.getMessage("scheduleNotType", Track.SCHEDULE, getName(), car.getTypeName()); 442 } 443 444 // search schedule for a match 445 for (int i = 0; i < getSize(); i++) { 446 ScheduleItem si = track.getNextScheduleItem(); 447 if (debugFlag) { 448 log.debug("Item id: ({}) requesting type ({}) load ({}) final dest ({}, {})", si.getId(), 449 si.getTypeName(), si.getReceiveLoadName(), si.getDestinationName(), 450 si.getDestinationTrackName()); // NOI18N 451 } 452 String status = checkScheduleItem(si, car, track); 453 if (status.equals(Track.OKAY)) { 454 log.debug("Found item match ({}) car ({}) type ({}) load ({}) ship ({}) destination ({}, {})", 455 si.getId(), car.toString(), car.getTypeName(), si.getReceiveLoadName(), si.getShipLoadName(), 456 si.getDestinationName(), si.getDestinationTrackName()); // NOI18N 457 // remember which item was a match 458 car.setScheduleItemId(si.getId()); 459 return Track.OKAY; 460 } else { 461 if (debugFlag) { 462 log.debug("Item id: ({}) status ({})", si.getId(), status); 463 } 464 } 465 } 466 if (debugFlag) { 467 log.debug("No Match"); 468 } 469 car.setScheduleItemId(Car.NONE); // clear the car's schedule id 470 return Bundle.getMessage("matchMessage", Track.SCHEDULE, getName(), 471 hasRandomItem() ? Bundle.getMessage("Random") : ""); 472 } 473 474 public String checkScheduleItem(ScheduleItem si, Car car, Track track) { 475 // if car is already assigned to this schedule item allow it to be 476 // dropped off on the wrong day (car arrived late) 477 if (!car.getScheduleItemId().equals(si.getId()) && 478 !si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) && 479 !InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 480 .equals(si.getSetoutTrainScheduleId())) { 481 TrainSchedule trainSch = InstanceManager.getDefault(TrainScheduleManager.class) 482 .getScheduleById(si.getSetoutTrainScheduleId()); 483 if (trainSch != null) { 484 return Bundle.getMessage("requestCarOnly", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(), 485 trainSch.getName()); 486 } 487 } 488 // Check for correct car type 489 if (!car.getTypeName().equals(si.getTypeName())) { 490 return Bundle.getMessage("requestCarType", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName()); 491 } 492 // Check for correct car road 493 if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) { 494 return Bundle.getMessage("requestCar", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(), Track.ROAD, 495 si.getRoadName()); 496 } 497 // Check for correct car load 498 if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && !car.getLoadName().equals(si.getReceiveLoadName())) { 499 return Bundle.getMessage("requestCar", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(), Track.LOAD, 500 si.getReceiveLoadName()); 501 } 502 // don't try the random feature if car is already assigned to this 503 // schedule item 504 if (car.getFinalDestinationTrack() != track && 505 !si.getRandom().equals(ScheduleItem.NONE) && 506 !car.getScheduleItemId().equals(si.getId())) { 507 if (!si.doRandom()) { 508 return Bundle.getMessage("scheduleRandom", Track.SCHEDULE, getName(), si.getId(), si.getRandom(), si.getCalculatedRandom()); 509 } 510 } 511 return Track.OKAY; 512 } 513 514 public boolean checkScheduleAttribute(String attribute, String carType, Car car) { 515 List<ScheduleItem> scheduleItems = getItemsBySequenceList(); 516 for (ScheduleItem si : scheduleItems) { 517 if (si.getTypeName().equals(carType)) { 518 // check to see if schedule services car type 519 if (attribute.equals(Track.TYPE)) { 520 return true; 521 } 522 // check to see if schedule services car type and load 523 if (attribute.equals(Track.LOAD) && 524 (si.getReceiveLoadName().equals(ScheduleItem.NONE) || 525 car == null || 526 si.getReceiveLoadName().equals(car.getLoadName()))) { 527 return true; 528 } 529 // check to see if schedule services car type and road 530 if (attribute.equals(Track.ROAD) && 531 (si.getRoadName().equals(ScheduleItem.NONE) || 532 car == null || 533 si.getRoadName().equals(car.getRoadName()))) { 534 return true; 535 } 536 // check to see if train schedule allows delivery 537 if (attribute.equals(Track.TRAIN_SCHEDULE) && 538 (si.getSetoutTrainScheduleId().isEmpty() || 539 InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 540 .equals(si.getSetoutTrainScheduleId()))) { 541 return true; 542 } 543 // check to see if at least one schedule item can service car 544 if (attribute.equals(Track.ALL) && 545 (si.getReceiveLoadName().equals(ScheduleItem.NONE) || 546 car == null || 547 si.getReceiveLoadName().equals(car.getLoadName())) && 548 (si.getRoadName().equals(ScheduleItem.NONE) || 549 car == null || 550 si.getRoadName().equals(car.getRoadName())) && 551 (si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) || 552 InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId() 553 .equals(si.getSetoutTrainScheduleId()))) { 554 return true; 555 } 556 } 557 } 558 return false; 559 } 560 561 /** 562 * Construct this Entry from XML. This member has to remain synchronized 563 * with the detailed DTD in operations-config.xml 564 * 565 * @param e Consist XML element 566 */ 567 public Schedule(Element e) { 568 org.jdom2.Attribute a; 569 if ((a = e.getAttribute(Xml.ID)) != null) { 570 _id = a.getValue(); 571 } else { 572 log.warn("no id attribute in schedule element when reading operations"); 573 } 574 if ((a = e.getAttribute(Xml.NAME)) != null) { 575 _name = a.getValue(); 576 } 577 if ((a = e.getAttribute(Xml.COMMENT)) != null) { 578 _comment = a.getValue(); 579 } 580 if (e.getChildren(Xml.ITEM) != null) { 581 List<Element> eScheduleItems = e.getChildren(Xml.ITEM); 582 log.debug("schedule: {} has {} items", getName(), eScheduleItems.size()); 583 for (Element eScheduleItem : eScheduleItems) { 584 register(new ScheduleItem(eScheduleItem)); 585 } 586 } 587 } 588 589 /** 590 * Create an XML element to represent this Entry. This member has to remain 591 * synchronized with the detailed DTD in operations-config.xml. 592 * 593 * @return Contents in a JDOM Element 594 */ 595 public org.jdom2.Element store() { 596 Element e = new org.jdom2.Element(Xml.SCHEDULE); 597 e.setAttribute(Xml.ID, getId()); 598 e.setAttribute(Xml.NAME, getName()); 599 e.setAttribute(Xml.COMMENT, getComment()); 600 for (ScheduleItem si : getItemsBySequenceList()) { 601 e.addContent(si.store()); 602 } 603 604 return e; 605 } 606 607 @Override 608 public void propertyChange(java.beans.PropertyChangeEvent e) { 609 if (Control.SHOW_PROPERTY) { 610 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e 611 .getNewValue()); 612 } 613 // forward all schedule item changes 614 setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue()); 615 } 616 617 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 618 // set dirty 619 InstanceManager.getDefault(LocationManagerXml.class).setDirty(true); 620 firePropertyChange(p, old, n); 621 } 622 623 private final static Logger log = LoggerFactory.getLogger(Schedule.class); 624 625}